diff --git a/src/App.tsx b/src/App.tsx index 29ab83b..d21929a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,28 @@ -import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom" +import { + BrowserRouter, + Navigate, + Outlet, + Route, + Routes, + useLocation, +} from "react-router-dom" import { Header } from "./pages/template/Header.tsx" import "./style/app.css" -import { lazy, ReactNode, Suspense } from "react" +import { + createContext, + lazy, + ReactNode, + Suspense, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from "react" import { BASE } from "./Constants.ts" +import { Authentication, Fetcher } from "./app/Fetcher.ts" +import { User } from "./model/User.ts" const HomePage = lazy(() => import("./pages/HomePage.tsx")) const LoginPage = lazy(() => import("./pages/LoginPage.tsx")) @@ -13,6 +32,9 @@ const CreateTeamPage = lazy(() => import("./pages/CreateTeamPage.tsx")) const TeamPanelPage = lazy(() => import("./pages/TeamPanel.tsx")) const NewTacticPage = lazy(() => import("./pages/NewTacticPage.tsx")) const Editor = lazy(() => import("./pages/Editor.tsx")) +const Settings = lazy(() => import("./pages/Settings.tsx")) + +const TOKEN_REFRESH_INTERVAL_MS = 60 * 1000 export default function App() { function suspense(node: ReactNode) { @@ -23,57 +45,196 @@ export default function App() { ) } + const fetcher = useMemo(() => new Fetcher(getStoredAuthentication()), []) + const [user, setUser] = useState(null) + + const handleAuthSuccess = useCallback( + async (auth: Authentication) => { + fetcher.updateAuthentication(auth) + const user = await fetchUser(fetcher) + setUser(user) + storeAuthentication(auth) + }, + [fetcher], + ) + + useEffect(() => { + const interval = setInterval(() => { + fetcher.fetchAPIGet("auth/keep-alive") + }, TOKEN_REFRESH_INTERVAL_MS) + + return () => clearInterval(interval) + }, [fetcher]) + return (
- - - - - )} /> - )} - /> - - )}> - )} /> - )} - /> - - )} - /> - )} - /> - )} - /> - )} - /> - )} - /> - - )} - /> - - - + + + + + + + , + )} + /> + , + )} + /> + + )}> + + + , + )} + /> + + + , + )} + /> + + + + , + )} + /> + + )} + /> + + + , + )} + /> + )} + /> + + + , + )} + /> + , + )} + /> + + )} + /> + + + + +
) } +async function fetchUser(fetcher: Fetcher): Promise { + const response = await fetcher.fetchAPIGet("user") + + if (!response.ok) { + throw Error( + "Could not retrieve user information : " + (await response.text()), + ) + } + + return await response.json() +} + +const STORAGE_AUTH_KEY = "token" + +function getStoredAuthentication(): Authentication { + const storedUser = localStorage.getItem(STORAGE_AUTH_KEY) + return storedUser == null ? null : JSON.parse(storedUser) +} + +function storeAuthentication(auth: Authentication) { + localStorage.setItem(STORAGE_AUTH_KEY, JSON.stringify(auth)) +} + +interface LoggedInPageProps { + children: ReactNode +} + +enum UserFetchingState { + FETCHING, + FETCHED, + ERROR, +} + +function LoggedInPage({ children }: LoggedInPageProps) { + const [user, setUser] = useUser() + const fetcher = useAppFetcher() + const [userFetchingState, setUserFetchingState] = useState( + user === null ? UserFetchingState.FETCHING : UserFetchingState.FETCHED, + ) + const location = useLocation() + + useEffect(() => { + async function initUser() { + try { + const user = await fetchUser(fetcher) + setUser(user) + setUserFetchingState(UserFetchingState.FETCHED) + } catch (e) { + setUserFetchingState(UserFetchingState.ERROR) + } + } + + if (userFetchingState === UserFetchingState.FETCHING) initUser() + }, [fetcher, setUser, userFetchingState]) + + switch (userFetchingState) { + case UserFetchingState.ERROR: + return ( + + ) + case UserFetchingState.FETCHED: + return children + case UserFetchingState.FETCHING: + return

Fetching user...

+ } +} + function AppLayout() { return ( <> @@ -82,3 +243,20 @@ function AppLayout() { ) } + +interface UserContext { + user: User | null + setUser: (user: User | null) => void +} + +const SignedInUserContext = createContext(null) +const FetcherContext = createContext(new Fetcher()) + +export function useAppFetcher() { + return useContext(FetcherContext) +} + +export function useUser(): [User | null, (user: User | null) => void] { + const { user, setUser } = useContext(SignedInUserContext)! + return [user, setUser] +} diff --git a/src/Fetcher.ts b/src/Fetcher.ts deleted file mode 100644 index 0311121..0000000 --- a/src/Fetcher.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { API } from "./Constants" -import { getSession, saveSession, Session } from "./api/session.ts" - -export async function fetchAPI( - url: string, - payload: unknown, - method = "POST", -): Promise { - const session = getSession() - const token = session?.auth?.token - - const headers: HeadersInit = { - Accept: "application/json", - "Content-Type": "application/json", - } - - if (token) { - headers.Authorization = token - } - - const response = await fetch(`${API}/${url}`, { - method, - headers, - body: JSON.stringify(payload), - }) - - return await handleResponse(session, response) -} - -export async function fetchAPIGet(url: string): Promise { - const session = getSession() - const token = session?.auth?.token - - const headers: HeadersInit = { - Accept: "application/json", - "Content-Type": "application/json", - } - - if (token) { - headers.Authorization = token - } - - const response = await fetch(`${API}/${url}`, { - method: "GET", - headers, - }) - - return await handleResponse(session, response) -} - -async function handleResponse( - session: Session, - response: Response, -): Promise { - // if we provided a token but still unauthorized, the token has expired - if (!response.ok) { - return response - } - - const nextToken = response.headers.get("Next-Authorization")! - const expirationDate = new Date( - response.headers.get("Next-Authorization-Expiration-Date")!, - ) - if (nextToken && expirationDate) - saveSession({ ...session, auth: { token: nextToken, expirationDate } }) - - return response -} diff --git a/src/api/session.ts b/src/api/session.ts deleted file mode 100644 index 3eb3035..0000000 --- a/src/api/session.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface Session { - auth?: Authentication - urlTarget?: string - username?: string -} - -export interface Authentication { - token: string - expirationDate: Date -} - -const SESSION_KEY = "session" - -// export const SessionContext = createContext(getSession()) - -export function saveSession(session: Session) { - localStorage.setItem(SESSION_KEY, JSON.stringify(session)) -} - -export function getSession(): Session { - const json = localStorage.getItem(SESSION_KEY) - return json ? JSON.parse(json) : {} -} diff --git a/src/api/failure.ts b/src/app/Failure.ts similarity index 100% rename from src/api/failure.ts rename to src/app/Failure.ts diff --git a/src/app/Fetcher.ts b/src/app/Fetcher.ts new file mode 100644 index 0000000..55e8f44 --- /dev/null +++ b/src/app/Fetcher.ts @@ -0,0 +1,78 @@ +import { API } from "../Constants.ts" + +export interface Authentication { + token: string + expirationDate: Date +} + +export class Fetcher { + private auth?: Authentication + + public constructor(auth?: Authentication) { + this.auth = auth + } + + async fetchAPI( + url: string, + payload: unknown, + method = "POST", + ): Promise { + const token = this.auth?.token + + const headers: HeadersInit = { + Accept: "application/json", + "Content-Type": "application/json", + } + + if (token) { + headers.Authorization = token + } + + const response = await fetch(`${API}/${url}`, { + method, + headers, + body: JSON.stringify(payload), + }) + + return await this.handleResponse(response) + } + + async fetchAPIGet(url: string): Promise { + const token = this.auth?.token + + const headers: HeadersInit = { + Accept: "application/json", + "Content-Type": "application/json", + } + + if (token) { + headers.Authorization = token + } + + const response = await fetch(`${API}/${url}`, { + method: "GET", + headers, + }) + + return await this.handleResponse(response) + } + + updateAuthentication(auth: Authentication) { + this.auth = auth + } + + async handleResponse(response: Response): Promise { + // if we provided a token but still unauthorized, the token has expired + if (!response.ok) { + return response + } + + const nextToken = response.headers.get("Next-Authorization")! + const expirationDate = response.headers.get("Next-Authorization-Expiration-Date")! + if (nextToken && expirationDate) { + this.auth = { token: nextToken, expirationDate: new Date(expirationDate) } + } + + return response + } +} diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index 4b1f2f7..f86db7c 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -95,6 +95,7 @@ import { LocalStorageTacticService } from "../service/LocalStorageTacticService. import { APITacticService } from "../service/APITacticService.ts" import { useParams } from "react-router-dom" import { ContentVersions } from "../editor/ContentVersions.ts" +import { useAppFetcher } from "../App.tsx" const ERROR_STYLE: CSSProperties = { borderColor: "red", @@ -132,12 +133,16 @@ interface EditorService { function EditorPortal({ guestMode }: EditorPageProps) { const { tacticId: idStr } = useParams() - + const fetcher = useAppFetcher() if (guestMode || !idStr) { return } - return + return ( + + ) } function EditorPageWrapper({ service }: { service: TacticService }) { diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 69968da..1a0d656 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,9 +1,8 @@ import "../style/home/home.css" -import { getSession } from "../api/session.ts" import { useNavigate } from "react-router-dom" import { useEffect, useState } from "react" import { User } from "../model/User.ts" -import { fetchAPIGet } from "../Fetcher.ts" +import { useAppFetcher } from "../App.tsx" interface Tactic { id: number @@ -27,17 +26,11 @@ export default function HomePage() { }) const navigate = useNavigate() + const fetcher = useAppFetcher() useEffect(() => { - const session = getSession() - - if (!session.auth) { - navigate("/login") - return - } - - async function getUser() { - const response = await fetchAPIGet("user-data") + async function initUserData() { + const response = await fetcher.fetchAPIGet("user-data") if (response.status == 401) { navigate("/login") return // if unauthorized @@ -45,8 +38,8 @@ export default function HomePage() { setInfo(await response.json()) } - getUser() - }, [navigate]) + initUserData() + }, [fetcher, navigate]) tactics!.sort((a, b) => b.creationDate - a.creationDate) diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index ddab7f4..26bf713 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -1,14 +1,20 @@ import { FormEvent, useState } from "react" -import { fetchAPI } from "../Fetcher.ts" -import { Failure } from "../api/failure.ts" -import { getSession, saveSession } from "../api/session.ts" +import { Failure } from "../app/Failure.ts" import "../style/form.css" -import { Link, useNavigate } from "react-router-dom" +import { Link, useLocation, useNavigate } from "react-router-dom" +import { useAppFetcher } from "../App.tsx" +import { Authentication } from "../app/Fetcher.ts" -export default function LoginApp() { +export interface LoginAppProps { + onSuccess: (auth: Authentication) => void +} + +export default function LoginApp({ onSuccess }: LoginAppProps) { const [errors, setErrors] = useState([]) + const fetcher = useAppFetcher() const navigate = useNavigate() + const location = useLocation() async function handleSubmit(e: FormEvent) { e.preventDefault() @@ -17,21 +23,17 @@ export default function LoginApp() { new FormData(e.target as HTMLFormElement), ) - const response = await fetchAPI( + const response = await fetcher.fetchAPI( "auth/token", { email, password }, "POST", ) if (response.ok) { - const session = getSession() const { token, expirationDate } = await response.json() - saveSession({ - ...session, - auth: { token, expirationDate: new Date(expirationDate) }, - urlTarget: undefined, - }) - navigate(session.urlTarget ?? "/") + const auth = { token, expirationDate: new Date(expirationDate) } + onSuccess(auth) + navigate(location.state?.from || "/") return } diff --git a/src/pages/NewTacticPage.tsx b/src/pages/NewTacticPage.tsx index 35db2a4..68e4be1 100644 --- a/src/pages/NewTacticPage.tsx +++ b/src/pages/NewTacticPage.tsx @@ -5,9 +5,8 @@ import plainCourt from "../assets/court/full_court.svg" import halfCourt from "../assets/court/half_court.svg" import { CourtType } from "../model/tactic/Tactic.ts" import { useCallback } from "react" -import { fetchAPI } from "../Fetcher.ts" -import { getSession, saveSession } from "../api/session.ts" -import { useLocation, useNavigate } from "react-router-dom" +import { useAppFetcher, useUser } from "../App.tsx" +import { useNavigate } from "react-router-dom" export const DEFAULT_TACTIC_NAME = "Nouvelle tactique" @@ -45,18 +44,19 @@ function CourtKindButton({ courtType: CourtType }) { const navigate = useNavigate() - const location = useLocation() + const fetcher = useAppFetcher() + const [user] = useUser() return (
{ // if user is not authenticated - if (!getSession().auth) { + if (!user) { navigate(`/tactic/edit-guest`) } - const response = await fetchAPI( + const response = await fetcher.fetchAPI( "tactics", { name: DEFAULT_TACTIC_NAME, @@ -66,10 +66,6 @@ function CourtKindButton({ ) if (response.status === 401) { - saveSession({ - ...getSession(), - urlTarget: location.pathname, - }) // if unauthorized navigate("/login") return @@ -77,7 +73,7 @@ function CourtKindButton({ const { id } = await response.json() navigate(`/tactic/${id}/edit`) - }, [courtType, location.pathname, navigate])}> + }, [courtType, fetcher, navigate, user])}>
void +} + +export default function RegisterPage({ onSuccess }: RegisterPageProps) { const usernameField = useRef(null) const passwordField = useRef(null) const confirmpasswordField = useRef(null) const emailField = useRef(null) const [errors, setErrors] = useState([]) - const navigate = useNavigate() + const location = useLocation() + + const fetcher = useAppFetcher() async function handleSubmit(e: FormEvent) { e.preventDefault() @@ -36,7 +42,7 @@ export default function RegisterPage() { return } - const response = await fetchAPI("auth/register", { + const response = await fetcher.fetchAPI("auth/register", { username, password, email, @@ -44,13 +50,10 @@ export default function RegisterPage() { if (response.ok) { const { token, expirationDate } = await response.json() - const session = getSession() - saveSession({ - ...session, - auth: { token, expirationDate: new Date(expirationDate) }, - urlTarget: undefined, - }) - navigate(session.urlTarget ?? "/") + const auth = { token, expirationDate: new Date(expirationDate) } + navigate(location.state?.from || "/") + + onSuccess(auth) return } diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx new file mode 100644 index 0000000..0eff7a0 --- /dev/null +++ b/src/pages/Settings.tsx @@ -0,0 +1,278 @@ +import { FormEvent, useCallback, useEffect, useRef, useState } from "react" +import "../style/settings.css" +import { useAppFetcher, useUser } from "../App.tsx" +import { Fetcher } from "../app/Fetcher.ts" + +export default function ProfileSettings() { + const fetcher = useAppFetcher() + const [user, setUser] = useUser() + + const [errorMessages, setErrorMessages] = useState([]) + const [success, setSuccess] = useState(false) + + const [name, setName] = useState(user!.name) + const [email, setEmail] = useState(user!.email) + const [password, setPassword] = useState() + const [confirmPassword, setConfirmPassword] = useState() + + const passwordConfirmRef = useRef(null) + const formRef = useRef(null) + + const submitForm = useCallback( + async (e: FormEvent) => { + e.preventDefault() + + passwordConfirmRef.current!.checkValidity() + if (password !== confirmPassword) { + setErrorMessages(["Les mots de passe ne correspondent pas !"]) + return + } + + const req: AccountUpdateRequest = { + name: name, + email: email, + } + if (password && password.length !== 0) { + req.password = password + } + const errors = await updateAccount(fetcher, req) + if (errors.length !== 0) { + setErrorMessages(errors) + setSuccess(false) + return + } + + setUser({ ...user!, email, name }) + setSuccess(true) + formRef.current!.reset() + setErrorMessages([]) + }, + [confirmPassword, email, fetcher, name, password, setUser, user], + ) + + useEffect(() => { + passwordConfirmRef.current!.setCustomValidity( + password === confirmPassword ? "" : "Les mots de passe ne correspondent pas !" + ) + }, [confirmPassword, password]) + + const [modalShow, setModalShow] = useState(false) + const width = 150 + + const profilePicture = user!.profilePicture + return ( +
+
+ {errorMessages.map((msg) => ( +
+ {msg} +
+ ))} + {success && ( +

+ Modifications sauvegardées +

+ )} +
+
+
+
+ profile-picture +
+ + setModalShow(false)} + /> +
+
+
+
+ + setName(e.target.value)} + /> + + + setEmail(e.target.value)} + /> + + + setPassword(e.target.value)} + /> + + + setConfirmPassword(e.target.value)} + /> + + +
+
+
+
+
+ ) +} + +interface AccountUpdateRequest { + name?: string + email?: string + profilePicture?: string + password?: string +} + +async function updateAccount( + fetcher: Fetcher, + req: AccountUpdateRequest, +): Promise { + const response = await fetcher.fetchAPI("user", req, "PUT") + if (response.ok) return [] + const body = await response.json() + return Object.entries(body).flatMap(([kind, messages]) => + (messages as string[]).map((msg) => `${kind}: ${msg}`), + ) +} + +interface ProfileImageInputPopupProps { + show: boolean + onHide: () => void +} + +function ProfileImageInputPopup({ show, onHide }: ProfileImageInputPopupProps) { + const urlRef = useRef(null) + const [errorMessages, setErrorMessages] = useState() + + const fetcher = useAppFetcher() + const [user, setUser] = useUser() + + const [link, setLink] = useState("") + + useEffect(() => { + function onKeyUp(e: KeyboardEvent) { + if (e.key === "Escape") onHide() + } + + window.addEventListener('keyup', onKeyUp) + return () => window.removeEventListener('keyup', onKeyUp) + }, [onHide]) + + const handleForm = useCallback(async (e: FormEvent) => { + e.preventDefault() + + const url = urlRef.current!.value + const errors = await updateAccount(fetcher, { + profilePicture: url, + }) + if (errors.length !== 0) { + setErrorMessages(errors) + return + } + setUser({ ...user!, profilePicture: url }) + setErrorMessages([]) + onHide() + }, [fetcher, onHide, setUser, user]) + + if (!show) return <> + + return ( + +
+
+

+ Nouvelle photo de profil +

+
+ + {errorMessages?.map((msg) => ( +
+ {msg} +
+ ))} + + setLink(e.target.value)} + /> + +
+
+ ) +} diff --git a/src/pages/template/Header.tsx b/src/pages/template/Header.tsx index 24cb4ad..9056ea8 100644 --- a/src/pages/template/Header.tsx +++ b/src/pages/template/Header.tsx @@ -1,38 +1,21 @@ import AccountSvg from "../../assets/account.svg?react" import "../../style/template/header.css" -import { useEffect, useState } from "react" -import { fetchAPIGet } from "../../Fetcher.ts" -import { getSession, saveSession } from "../../api/session.ts" -import { useLocation, useNavigate } from "react-router-dom" +import { useUser } from "../../App.tsx" +import { useNavigate } from "react-router-dom" export function Header() { - const session = getSession() - const [username, setUsername] = useState( - session.username ?? null, - ) - const navigate = useNavigate() - const location = useLocation() - - useEffect(() => { - async function loadUsername() { - const response = await fetchAPIGet("user") - - if (response.status === 401) { - //if unauthorized, the header will display a login button instead, - //based on the nullity of username - return - } - - //TODO check if the response is ok and handle errors - const { name: username } = await response.json() - saveSession({ ...session, username }) - setUsername(username) - } - - // if the user is authenticated and the username is not already present in the session, - if (session.auth && !session.username) loadUsername() - }, [session]) + const [user] = useUser() + + const accountImage = user?.profilePicture ? ( + {`Photo + ) : ( + + ) return (
diff --git a/src/service/APITacticService.ts b/src/service/APITacticService.ts index 82fd8bb..3014294 100644 --- a/src/service/APITacticService.ts +++ b/src/service/APITacticService.ts @@ -1,19 +1,25 @@ import { TacticService, ServiceError, TacticContext } from "./TacticService.ts" import { StepContent, StepInfoNode } from "../model/tactic/Tactic.ts" -import { fetchAPI, fetchAPIGet } from "../Fetcher.ts" +import { Fetcher } from "../app/Fetcher.ts" export class APITacticService implements TacticService { private readonly tacticId: number + private readonly fetcher: Fetcher - constructor(tacticId: number) { + constructor(fetcher: Fetcher, tacticId: number) { this.tacticId = tacticId + this.fetcher = fetcher } async getContext(): Promise { - const infoResponsePromise = fetchAPIGet(`tactics/${this.tacticId}`) + const infoResponsePromise = this.fetcher.fetchAPIGet( + `tactics/${this.tacticId}`, + ) const infoResponse = await infoResponsePromise - const treeResponsePromise = fetchAPIGet(`tactics/${this.tacticId}/tree`) + const treeResponsePromise = this.fetcher.fetchAPIGet( + `tactics/${this.tacticId}/tree`, + ) const treeResponse = await treeResponsePromise if (infoResponse.status == 401 || treeResponse.status == 401) { @@ -29,10 +35,13 @@ export class APITacticService implements TacticService { parent: StepInfoNode, content: StepContent, ): Promise { - const response = await fetchAPI(`tactics/${this.tacticId}/steps`, { - parentId: parent.id, - content, - }) + const response = await this.fetcher.fetchAPI( + `tactics/${this.tacticId}/steps`, + { + parentId: parent.id, + content, + }, + ) if (response.status == 404) return ServiceError.NOT_FOUND if (response.status == 401) return ServiceError.UNAUTHORIZED @@ -41,7 +50,7 @@ export class APITacticService implements TacticService { } async removeStep(id: number): Promise { - const response = await fetchAPI( + const response = await this.fetcher.fetchAPI( `tactics/${this.tacticId}/steps/${id}`, {}, "DELETE", @@ -51,7 +60,7 @@ export class APITacticService implements TacticService { } async setName(name: string): Promise { - const response = await fetchAPI( + const response = await this.fetcher.fetchAPI( `tactics/${this.tacticId}/name`, { name }, "PUT", @@ -64,7 +73,7 @@ export class APITacticService implements TacticService { step: number, content: StepContent, ): Promise { - const response = await fetchAPI( + const response = await this.fetcher.fetchAPI( `tactics/${this.tacticId}/steps/${step}`, { content }, "PUT", @@ -74,7 +83,7 @@ export class APITacticService implements TacticService { } async getContent(step: number): Promise { - const response = await fetchAPIGet( + const response = await this.fetcher.fetchAPIGet( `tactics/${this.tacticId}/steps/${step}`, ) if (response.status == 404) return ServiceError.NOT_FOUND diff --git a/src/style/home/home.css b/src/style/home/home.css index 3af258a..479773a 100644 --- a/src/style/home/home.css +++ b/src/style/home/home.css @@ -20,13 +20,13 @@ body { #body { display: flex; flex-direction: row; - margin: 0px; + margin: 0; height: 100%; background-color: var(--home-second-color); } .data { - border: 1.5px solid var(--main-contrast-color); + border: 2px solid var(--main-contrast-color); background-color: var(--home-main-color); border-radius: 0.75cap; color: var(--main-contrast-color); diff --git a/src/style/settings.css b/src/style/settings.css new file mode 100644 index 0000000..8c2f449 --- /dev/null +++ b/src/style/settings.css @@ -0,0 +1,120 @@ +.error-message { + color: red; +} + +.success-message { + color: green; +} + +#settings-page { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +#profile-picture { + display: flex; + justify-content: center; +} + +#settings-content { + padding: 30px; + border: 1px solid gray; + border-radius: 15px; + background-color: #e1e1e1; +} + +#settings-inputs { + display: flex; + gap: 20px; +} + +.settings-button { + background-color: white; + border: solid 1px #0d6efd; + border-radius: 5px; + color: #0d6efd; + padding: 5px; + transition: background-color 250ms; +} + +.settings-button:hover { + background-color: #0d6efd; + color: white; + cursor: pointer; +} + +.settings-input { + border: solid 1px lightgray; + border-radius: 5px; + padding: 5px; + outline: #5f8fee; +} + +.settings-input:focus { + box-shadow: #0056b3; +} + +#credentials-form { + display: flex; + flex-direction: column; +} + +#profile-picture-popup { + display: flex; + + flex-direction: column; + align-items: center; + justify-content: center; + + position: absolute; + top: 0; + left: 0; + z-index: 100000000000000000000000000000000; + width: 100%; + height: 100%; + + background-color: rgba(84, 78, 78, 0.33); +} + +#profile-picture-popup-form { + display: flex; + flex-direction: column; + row-gap: 20px; + + background-color: white; + padding: 25px; + border-radius: 25px; +} + +#profile-picture-popup-title { + font-size: 25px; +} + +#profile-picture-popup-subtitle, +#profile-picture-popup-title { + margin: 0; +} + +#profile-picture-popup-header { + border: 0 solid; + border-bottom-width: 1px; +} + +#profile-picture-popup-footer { + display: flex; + width: 100%; + gap: 5px; +} + +#profile-picture-img { + user-select: none; + -webkit-user-drag: none; + border-radius: 2000px; +} + +.invalid-input { + border: 1px solid red; +} diff --git a/src/style/template/header.css b/src/style/template/header.css index dba7305..6d64f46 100644 --- a/src/style/template/header.css +++ b/src/style/template/header.css @@ -16,6 +16,9 @@ #img-account { cursor: pointer; margin-right: 5px; + width: 50px; + height: 50px; + border-radius: 20%; } #header-left, @@ -29,8 +32,8 @@ flex-direction: column; justify-content: center; align-items: end; + margin-right: 30px; color: white; - margin-right: 5px; } #clickable-header-right:hover #username {