From 6738ddcb6769ff57b372c31b43b170f8e54aa8d1 Mon Sep 17 00:00:00 2001 From: maxime Date: Sun, 24 Mar 2024 13:32:05 +0100 Subject: [PATCH] add settings page, refactor session handling --- index.html | 12 ++ package.json | 2 + src/App.tsx | 244 ++++++++++++++++++++----- src/Fetcher.ts | 68 ------- src/api/session.ts | 23 --- src/{api/failure.ts => app/Failure.ts} | 0 src/app/Fetcher.ts | 87 +++++++++ src/pages/Editor.tsx | 119 +++++------- src/pages/HomePage.tsx | 19 +- src/pages/LoginPage.tsx | 29 +-- src/pages/NewTacticPage.tsx | 18 +- src/pages/RegisterPage.tsx | 31 ++-- src/pages/Settings.tsx | 222 ++++++++++++++++++++++ src/pages/template/Header.tsx | 46 +---- src/service/APITacticService.ts | 20 +- src/style/settings.css | 16 ++ src/style/template/header.css | 11 +- 17 files changed, 654 insertions(+), 313 deletions(-) delete mode 100644 src/Fetcher.ts delete mode 100644 src/api/session.ts rename src/{api/failure.ts => app/Failure.ts} (100%) create mode 100644 src/app/Fetcher.ts create mode 100644 src/pages/Settings.tsx create mode 100644 src/style/settings.css diff --git a/index.html b/index.html index e28377b..cd4f9e5 100644 --- a/index.html +++ b/index.html @@ -5,9 +5,21 @@ IQBall +
+ + + + + + + diff --git a/package.json b/package.json index d89f82b..33229a9 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,10 @@ "@testing-library/user-event": "^13.5.0", "@types/react": "^18.2.31", "@types/react-dom": "^18.2.14", + "bootstrap": "^5.3.3", "eslint-plugin-react-refresh": "^0.4.5", "react": "^18.2.0", + "react-bootstrap": "^2.10.2", "react-dom": "^18.2.0", "react-draggable": "^4.4.6", "react-router-dom": "^6.22.0", diff --git a/src/App.tsx b/src/App.tsx index 29ab83b..b947ccc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,11 @@ -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 +15,7 @@ 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")) export default function App() { function suspense(node: ReactNode) { @@ -23,57 +26,185 @@ export default function App() { ) } + const storedAuth = useMemo(() => getStoredAuthentication(), []) + const fetcher = useMemo(() => new Fetcher(storedAuth), [storedAuth]) + const [user, setUser] = useState(null) + + const handleAuthSuccess = useCallback(async (auth: Authentication) => { + fetcher.updateAuthentication(auth) + const user = await fetchUser(fetcher) + setUser(user) + storeAuthentication(auth) + }, [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 +213,22 @@ function AppLayout() { ) } + +interface UserContext { + user: User | null + setUser: (user: User) => void +} + +const SignedInUserContext = createContext(null) +const FetcherContext = createContext(new Fetcher()) + +export function useAppFetcher() { + return useContext(FetcherContext) +} + +export function useUser(): [User | null, (user: User) => 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..c925be8 --- /dev/null +++ b/src/app/Fetcher.ts @@ -0,0 +1,87 @@ +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 = new Date( + response.headers.get("Next-Authorization-Expiration-Date")!, + ) + if (nextToken && expirationDate) { + this.auth = { token: nextToken, expirationDate } + } + + return response + } +} + + diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index 4b1f2f7..ccd54d9 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -1,13 +1,4 @@ -import { - CSSProperties, - RefObject, - SetStateAction, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react" +import { CSSProperties, RefObject, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react" import "../style/editor.css" import TitleInput from "../components/TitleInput" import PlainCourt from "../assets/court/full_court.svg?react" @@ -18,18 +9,9 @@ import { BallPiece } from "../components/editor/BallPiece" import { Rack } from "../components/Rack" import { PlayerPiece } from "../components/editor/PlayerPiece" -import { - ComponentId, - CourtType, - StepContent, - StepInfoNode, - TacticComponent, -} from "../model/tactic/Tactic" - -import SavingState, { - SaveState, - SaveStates, -} from "../components/editor/SavingState" +import { ComponentId, CourtType, StepContent, StepInfoNode, TacticComponent } from "../model/tactic/Tactic" + +import SavingState, { SaveState, SaveStates } from "../components/editor/SavingState" import { BALL_TYPE } from "../model/tactic/CourtObjects" import { CourtAction } from "../components/editor/CourtAction" @@ -52,19 +34,10 @@ import { updateComponent, } from "../editor/TacticContentDomains" -import { - BallState, - Player, - PlayerInfo, - PlayerLike, - PlayerTeam, -} from "../model/tactic/Player" +import { BallState, Player, PlayerInfo, PlayerLike, PlayerTeam } from "../model/tactic/Player" import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems" -import { - CourtPlayer, - EditableCourtPlayer, -} from "../components/editor/CourtPlayer.tsx" +import { CourtPlayer, EditableCourtPlayer } from "../components/editor/CourtPlayer.tsx" import { createAction, getActionKind, @@ -76,25 +49,17 @@ import ArrowAction from "../components/actions/ArrowAction" import { middlePos, Pos, ratioWithinBase } from "../geo/Pos" import { Action, ActionKind } from "../model/tactic/Action" import BallAction from "../components/actions/BallAction" -import { - computePhantomPositioning, - getOrigin, - removePlayer, -} from "../editor/PlayerDomains" +import { computePhantomPositioning, getOrigin, removePlayer } from "../editor/PlayerDomains" import { CourtBall } from "../components/editor/CourtBall" import StepsTree from "../components/editor/StepsTree" -import { - addStepNode, - getParent, - getStepNode, - removeStepNode, -} from "../editor/StepsDomain" +import { addStepNode, getParent, getStepNode, removeStepNode } from "../editor/StepsDomain" import SplitLayout from "../components/SplitLayout.tsx" import { ServiceError, TacticService } from "../service/TacticService.ts" import { LocalStorageTacticService } from "../service/LocalStorageTacticService.ts" 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 +97,12 @@ interface EditorService { function EditorPortal({ guestMode }: EditorPageProps) { const { tacticId: idStr } = useParams() - + const fetcher = useAppFetcher() if (guestMode || !idStr) { return } - return + return } function EditorPageWrapper({ service }: { service: TacticService }) { @@ -245,7 +210,7 @@ function EditorPageWrapper({ service }: { service: TacticService }) { if (typeof contextResult === "string") { setPanicMessage( "There has been an error retrieving the editor initial context : " + - contextResult, + contextResult, ) return } @@ -260,7 +225,7 @@ function EditorPageWrapper({ service }: { service: TacticService }) { if (typeof contentResult === "string") { setPanicMessage( "There has been an error retrieving the tactic's root step content : " + - contentResult, + contentResult, ) return } @@ -523,15 +488,15 @@ function EditorPage({ /> ), !isFrozen && - (info.ballState === BallState.HOLDS_ORIGIN || - info.ballState === BallState.PASSED_ORIGIN) && ( - { - doMoveBall(ballBounds, player) - }} - /> - ), + (info.ballState === BallState.HOLDS_ORIGIN || + info.ballState === BallState.PASSED_ORIGIN) && ( + { + doMoveBall(ballBounds, player) + }} + /> + ), ] }, [ @@ -845,12 +810,12 @@ interface EditorStepsTreeProps { } function EditorStepsTree({ - selectedStepId, - root, - onAddChildren, - onRemoveNode, - onStepSelected, -}: EditorStepsTreeProps) { + selectedStepId, + root, + onAddChildren, + onRemoveNode, + onStepSelected, + }: EditorStepsTreeProps) { return (
courtRef.current!.getBoundingClientRect(), [courtRef], @@ -934,15 +899,15 @@ interface CourtPlayerArrowActionProps { } function CourtPlayerArrowAction({ - playerInfo, - player, - isInvalid, + playerInfo, + player, + isInvalid, - content, - setContent, - setPreviewAction, - courtRef, -}: CourtPlayerArrowActionProps) { + content, + setContent, + setPreviewAction, + courtRef, + }: CourtPlayerArrowActionProps) { const courtBounds = useCallback( () => courtRef.current!.getBoundingClientRect(), [courtRef], 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..421761f 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,18 @@ 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) + console.log(location) + 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..6093c86 --- /dev/null +++ b/src/pages/Settings.tsx @@ -0,0 +1,222 @@ +import "bootstrap/dist/css/bootstrap.min.css" +import Button from "react-bootstrap/Button" +import Form from "react-bootstrap/Form" +import Image from "react-bootstrap/Image" +import Container from "react-bootstrap/Container" +import Row from "react-bootstrap/Row" +import Col from "react-bootstrap/Col" +import Modal from "react-bootstrap/Modal" +import { Stack } from "react-bootstrap" +import { useRef, useState } from "react" +import "../style/settings.css" +import { useAppFetcher, useUser } from "../App.tsx" +import { Fetcher } from "../app/Fetcher.ts" + +export default function Settings() { + return ( +
+
+ +
+
+ ) +} + + +function AccountSettings() { + return ( +
+ +
+ ) +} + +function ProfilSettings() { + const nameRef = useRef(null) + const emailRef = useRef(null) + const passwordRef = useRef(null) + const confirmPasswordRef = useRef(null) + + const fetcher = useAppFetcher() + const [user, setUser] = useUser() + + const [errorMessages, setErrorMessages] = useState([]) + + + const [modalShow, setModalShow] = useState(false) + const width = 150 + + console.log(user) + + const profilePicture = user!.profilePicture + return ( + + {errorMessages.map(msg => (
{msg}
))} + + + +
+ +
+ + setModalShow(false)} + /> +
+ + + +
+ + Nom d'utilisateur + + + + Adresse mail + + + + + Mot de passe + + + + Confirmez le mot de passe + + + +
+ +
+
+ ) +} + +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() + + return ( + + + + Nouvelle photo de profil + + + + + {errorMessages?.map(msg =>
{msg}
)} + Saisissez le lien vers votre nouvelle photo de profil + +
+ + + + +
+ ) +} + + +async function imageExists(imageLink: string) { + try { + const response = await fetch(imageLink) + + console.log(response) + + if (response.ok) { + const contentType = response.headers.get("content-type") + return contentType?.startsWith("image/") ?? false + } + return false + } catch (error) { + console.error(error) + return false + } +} + diff --git a/src/pages/template/Header.tsx b/src/pages/template/Header.tsx index 24cb4ad..f1dff6d 100644 --- a/src/pages/template/Header.tsx +++ b/src/pages/template/Header.tsx @@ -1,38 +1,15 @@ 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) - } + const [user, ] = useUser() - // if the user is authenticated and the username is not already present in the session, - if (session.auth && !session.username) loadUsername() - }, [session]) + const accountImage = user?.profilePicture + ? {"profile-picture"} + : return (
diff --git a/src/service/APITacticService.ts b/src/service/APITacticService.ts index 82fd8bb..87c4a93 100644 --- a/src/service/APITacticService.ts +++ b/src/service/APITacticService.ts @@ -1,19 +1,21 @@ 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,7 +31,7 @@ export class APITacticService implements TacticService { parent: StepInfoNode, content: StepContent, ): Promise { - const response = await fetchAPI(`tactics/${this.tacticId}/steps`, { + const response = await this.fetcher.fetchAPI(`tactics/${this.tacticId}/steps`, { parentId: parent.id, content, }) @@ -41,7 +43,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 +53,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 +66,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 +76,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/settings.css b/src/style/settings.css new file mode 100644 index 0000000..446dc60 --- /dev/null +++ b/src/style/settings.css @@ -0,0 +1,16 @@ +.error-message { + color: red +} + +#settings-page { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +#profile-picture { + display: flex; + justify-content: center; +} \ No newline at end of file diff --git a/src/style/template/header.css b/src/style/template/header.css index dba7305..ee48daf 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,14 +32,20 @@ flex-direction: column; justify-content: center; align-items: end; + margin-right: 30px; color: white; - margin-right: 5px; } #clickable-header-right:hover #username { color: var(--accent-color); } +#username { + text-align: center; + vertical-align: center; + margin: 0; +} + #clickable-header-right { border-radius: 1cap;