From 62be8f2a0b98d345d542586ac4cf6a4caec09b9c Mon Sep 17 00:00:00 2001 From: maxime Date: Sun, 24 Mar 2024 21:55:34 +0100 Subject: [PATCH 1/6] fix guest mode --- src/pages/Editor.tsx | 1 + src/service/LocalStorageTacticService.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index a9ece7d..4b1f2f7 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -269,6 +269,7 @@ function EditorPageWrapper({ service }: { service: TacticService }) { stepsVersions.set(stepId, versions) versions.insertAndCut(contentResult) + console.log(contentResult) setStepContent(contentResult, false) } diff --git a/src/service/LocalStorageTacticService.ts b/src/service/LocalStorageTacticService.ts index daf05c4..2cce23d 100644 --- a/src/service/LocalStorageTacticService.ts +++ b/src/service/LocalStorageTacticService.ts @@ -23,6 +23,10 @@ export class LocalStorageTacticService implements TacticService { GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY, JSON.stringify({ id: 1, children: [] }), ) + localStorage.setItem( + GUEST_MODE_STEP_CONTENT_STORAGE_KEY + 1, + JSON.stringify({components: []}) + ) } return new LocalStorageTacticService() -- 2.36.3 From 6738ddcb6769ff57b372c31b43b170f8e54aa8d1 Mon Sep 17 00:00:00 2001 From: maxime Date: Sun, 24 Mar 2024 13:32:05 +0100 Subject: [PATCH 2/6] 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; -- 2.36.3 From fa7339b0f1eddec331d9d1d97b8d3f9df829682c Mon Sep 17 00:00:00 2001 From: maxime Date: Sun, 24 Mar 2024 20:00:02 +0100 Subject: [PATCH 3/6] add a keep alive session loop --- src/App.tsx | 12 ++++++++++++ src/pages/LoginPage.tsx | 1 - src/pages/Settings.tsx | 5 ----- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b947ccc..02f6989 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,8 @@ 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) { return ( @@ -37,6 +39,16 @@ export default function App() { storeAuthentication(auth) }, [fetcher]) + + useEffect(() => { + const interval = setInterval(() => { + fetcher.fetchAPIGet("auth/keep-alive") + console.log("KEPT ALIVE !") + }, TOKEN_REFRESH_INTERVAL_MS) + + return () => clearInterval(interval) + }, [fetcher]) + return (
diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 421761f..458e5dc 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -33,7 +33,6 @@ export default function LoginApp({onSuccess}: LoginAppProps) { const { token, expirationDate } = await response.json() const auth = { token, expirationDate: new Date(expirationDate) } onSuccess(auth) - console.log(location) navigate(location.state?.from || "/") return } diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 6093c86..4bf0c37 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -46,8 +46,6 @@ function ProfilSettings() { const [modalShow, setModalShow] = useState(false) const width = 150 - console.log(user) - const profilePicture = user!.profilePicture return ( @@ -101,7 +99,6 @@ function ProfilSettings() { const password = passwordRef.current?.value const confirmPassword = confirmPasswordRef.current?.value - console.log(password, confirmPassword, name, email) if (password !== confirmPassword) { setErrorMessages(["Les mots de passe ne correspondent pas !"]) return @@ -207,8 +204,6 @@ 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 -- 2.36.3 From 42c0300cedc5534be33755bbf0037b36d5203d7e Mon Sep 17 00:00:00 2001 From: maxime Date: Sun, 24 Mar 2024 21:28:54 +0100 Subject: [PATCH 4/6] remove bottstrap --- index.html | 9 +- package.json | 2 - src/App.tsx | 153 ++++++++------ src/app/Fetcher.ts | 9 +- src/pages/Editor.tsx | 120 +++++++---- src/pages/LoginPage.tsx | 2 +- src/pages/Settings.tsx | 354 ++++++++++++++++++-------------- src/pages/template/Header.tsx | 14 +- src/service/APITacticService.ts | 19 +- src/style/settings.css | 108 +++++++++- 10 files changed, 497 insertions(+), 293 deletions(-) diff --git a/index.html b/index.html index cd4f9e5..cd21ed8 100644 --- a/index.html +++ b/index.html @@ -5,11 +5,12 @@ IQBall -
- + - + diff --git a/package.json b/package.json index 33229a9..d89f82b 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,8 @@ "@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 02f6989..32b74ac 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,25 @@ -import { BrowserRouter, Navigate, Outlet, Route, Routes, useLocation } 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 { createContext, lazy, ReactNode, Suspense, useCallback, useContext, useEffect, useMemo, useState } 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" @@ -32,18 +49,19 @@ export default function App() { 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]) - + 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") - console.log("KEPT ALIVE !") }, TOKEN_REFRESH_INTERVAL_MS) return () => clearInterval(interval) @@ -52,93 +70,86 @@ export default function App() { return (
- + )} + element={suspense( + , + )} /> )} + element={suspense( + , + )} /> )}> - , - ) - } + )} /> - - , - ) - } + element={suspense( + + + , + )} /> - - , - ) - } + element={suspense( + + + , + )} /> ) - } + element={suspense()} /> - - , - ) - } + element={suspense( + + + , + )} /> - - , - ) - } + element={suspense()} /> - - , - ) - } + element={suspense( + + + , + )} /> )} + element={suspense( + , + )} /> { const response = await fetcher.fetchAPIGet("user") if (!response.ok) { - throw Error("Could not retrieve user information : " + await response.text()) + throw Error( + "Could not retrieve user information : " + (await response.text()), + ) } return await response.json() @@ -182,13 +195,15 @@ interface LoggedInPageProps { enum UserFetchingState { FETCHING, FETCHED, - ERROR + ERROR, } function LoggedInPage({ children }: LoggedInPageProps) { const [user, setUser] = useUser() const fetcher = useAppFetcher() - const [userFetchingState, setUserFetchingState] = useState(user === null ? UserFetchingState.FETCHING : UserFetchingState.FETCHED) + const [userFetchingState, setUserFetchingState] = useState( + user === null ? UserFetchingState.FETCHING : UserFetchingState.FETCHED, + ) const location = useLocation() useEffect(() => { @@ -200,16 +215,20 @@ function LoggedInPage({ children }: LoggedInPageProps) { } catch (e) { setUserFetchingState(UserFetchingState.ERROR) } - } - if (userFetchingState === UserFetchingState.FETCHING) - initUser() + if (userFetchingState === UserFetchingState.FETCHING) initUser() }, [fetcher, setUser, userFetchingState]) switch (userFetchingState) { case UserFetchingState.ERROR: - return + return ( + + ) case UserFetchingState.FETCHED: return children case UserFetchingState.FETCHING: @@ -242,5 +261,3 @@ export function useUser(): [User | null, (user: User) => void] { const { user, setUser } = useContext(SignedInUserContext)! return [user, setUser] } - - diff --git a/src/app/Fetcher.ts b/src/app/Fetcher.ts index c925be8..14b353e 100644 --- a/src/app/Fetcher.ts +++ b/src/app/Fetcher.ts @@ -1,13 +1,11 @@ import { API } from "../Constants.ts" - export interface Authentication { token: string expirationDate: Date } export class Fetcher { - private auth?: Authentication public constructor(auth?: Authentication) { @@ -63,10 +61,7 @@ export class Fetcher { this.auth = auth } - - async handleResponse( - response: Response, - ): Promise { + async handleResponse(response: Response): Promise { // if we provided a token but still unauthorized, the token has expired if (!response.ok) { return response @@ -83,5 +78,3 @@ export class Fetcher { return response } } - - diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index ccd54d9..f86db7c 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -1,4 +1,13 @@ -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" @@ -9,9 +18,18 @@ 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" @@ -34,10 +52,19 @@ 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, @@ -49,10 +76,19 @@ 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" @@ -102,7 +138,11 @@ function EditorPortal({ guestMode }: EditorPageProps) { return } - return + return ( + + ) } function EditorPageWrapper({ service }: { service: TacticService }) { @@ -210,7 +250,7 @@ function EditorPageWrapper({ service }: { service: TacticService }) { if (typeof contextResult === "string") { setPanicMessage( "There has been an error retrieving the editor initial context : " + - contextResult, + contextResult, ) return } @@ -225,7 +265,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 } @@ -488,15 +528,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) + }} + /> + ), ] }, [ @@ -810,12 +850,12 @@ interface EditorStepsTreeProps { } function EditorStepsTree({ - selectedStepId, - root, - onAddChildren, - onRemoveNode, - onStepSelected, - }: EditorStepsTreeProps) { + selectedStepId, + root, + onAddChildren, + onRemoveNode, + onStepSelected, +}: EditorStepsTreeProps) { return (
courtRef.current!.getBoundingClientRect(), [courtRef], @@ -899,15 +939,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/LoginPage.tsx b/src/pages/LoginPage.tsx index 458e5dc..26bf713 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -9,7 +9,7 @@ export interface LoginAppProps { onSuccess: (auth: Authentication) => void } -export default function LoginApp({onSuccess}: LoginAppProps) { +export default function LoginApp({ onSuccess }: LoginAppProps) { const [errors, setErrors] = useState([]) const fetcher = useAppFetcher() diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 4bf0c37..140af8f 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -1,129 +1,144 @@ -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 { FormEvent, useCallback, 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) - +export default function ProfileSettings() { const fetcher = useAppFetcher() const [user, setUser] = useUser() const [errorMessages, setErrorMessages] = useState([]) - + const [success, setSuccess] = useState(false) + + const formRef = useRef(null) + + const submitForm = useCallback( + async (e: FormEvent) => { + e.preventDefault() + const { name, email, password, confirmPassword } = + Object.fromEntries( + new FormData(formRef.current!) as Iterable< + [PropertyKey, string] + >, + ) + + 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([]) + }, + [fetcher, setUser, user], + ) const [modalShow, setModalShow] = useState(false) const width = 150 const profilePicture = user!.profilePicture return ( - - {errorMessages.map(msg => (
{msg}
))} - - - -
- +
+
+ {errorMessages.map((msg) => ( +
+ {msg} +
+ ))} + {success && ( +

+ Modifications sauvegardées +

+ )} +
+
+
+
+ profile-picture +
+ + setModalShow(false)} + />
- - setModalShow(false)} - /> - - - - -
- - Nom d'utilisateur - - - - Adresse mail - - - - - Mot de passe - - - - Confirmez le mot de passe - - - -
- - - +
+
+
+

Nom d'utilisateur

+ + +

Addresse email

+ + +

Mot de passe

+ + +

Confirmez le mot de passe

+ + + +
+
+
+
+
) } @@ -134,13 +149,16 @@ interface AccountUpdateRequest { password?: string } -async function updateAccount(fetcher: Fetcher, req: AccountUpdateRequest): Promise { +async function updateAccount( + fetcher: Fetcher, + req: AccountUpdateRequest, +): Promise { const response = await fetcher.fetchAPI("user", req, "PUT") - if (response.ok) - return [] + if (response.ok) return [] const body = await response.json() - return Object.entries(body) - .flatMap(([kind, messages]) => (messages as string[]).map(msg => `${kind}: ${msg}`)) + return Object.entries(body).flatMap(([kind, messages]) => + (messages as string[]).map((msg) => `${kind}: ${msg}`), + ) } interface ProfileImageInputPopupProps { @@ -155,57 +173,76 @@ function ProfileImageInputPopup({ show, onHide }: ProfileImageInputPopupProps) { const fetcher = useAppFetcher() const [user, setUser] = useUser() + if (!show) return <> + return ( - - - - Nouvelle photo de profil - - - - - {errorMessages?.map(msg =>
{msg}
)} - Saisissez le lien vers votre nouvelle photo de profil - -
- - - - -
+ ref={urlRef} + type="input" + placeholder={"lien vers une image"} + /> + +
+
) } - async function imageExists(imageLink: string) { try { - const response = await fetch(imageLink) + const response = await fetch(imageLink, { mode: "no-cors" }) if (response.ok) { - const contentType = response.headers.get("content-type") + const contentType = response.headers.get("Content-type") return contentType?.startsWith("image/") ?? false } return false @@ -214,4 +251,3 @@ async function imageExists(imageLink: string) { return false } } - diff --git a/src/pages/template/Header.tsx b/src/pages/template/Header.tsx index f1dff6d..26401b4 100644 --- a/src/pages/template/Header.tsx +++ b/src/pages/template/Header.tsx @@ -5,11 +5,17 @@ import { useNavigate } from "react-router-dom" export function Header() { const navigate = useNavigate() - const [user, ] = useUser() + const [user] = useUser() - const accountImage = user?.profilePicture - ? {"profile-picture"} - : + const accountImage = user?.profilePicture ? ( + {"profile-picture"} + ) : ( + + ) return ( ) } - -async function imageExists(imageLink: string) { - try { - const response = await fetch(imageLink, { mode: "no-cors" }) - - if (response.ok) { - const contentType = response.headers.get("Content-type") - return contentType?.startsWith("image/") ?? false - } - return false - } catch (error) { - console.error(error) - return false - } -} -- 2.36.3 From f9c42862e042f754d5abb005086298558c0ff0e6 Mon Sep 17 00:00:00 2001 From: maxime Date: Mon, 25 Mar 2024 23:02:22 +0100 Subject: [PATCH 6/6] apply suggestions --- index.html | 15 ---- src/App.tsx | 7 +- src/app/Fetcher.ts | 6 +- src/pages/Settings.tsx | 131 +++++++++++++++++++++++----------- src/pages/template/Header.tsx | 2 +- src/style/home/home.css | 4 +- src/style/settings.css | 2 +- src/style/template/header.css | 6 -- 8 files changed, 98 insertions(+), 75 deletions(-) diff --git a/index.html b/index.html index cd21ed8..e28377b 100644 --- a/index.html +++ b/index.html @@ -8,21 +8,6 @@
- - - - - - - diff --git a/src/App.tsx b/src/App.tsx index 32b74ac..d21929a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -45,8 +45,7 @@ export default function App() { ) } - const storedAuth = useMemo(() => getStoredAuthentication(), []) - const fetcher = useMemo(() => new Fetcher(storedAuth), [storedAuth]) + const fetcher = useMemo(() => new Fetcher(getStoredAuthentication()), []) const [user, setUser] = useState(null) const handleAuthSuccess = useCallback( @@ -247,7 +246,7 @@ function AppLayout() { interface UserContext { user: User | null - setUser: (user: User) => void + setUser: (user: User | null) => void } const SignedInUserContext = createContext(null) @@ -257,7 +256,7 @@ export function useAppFetcher() { return useContext(FetcherContext) } -export function useUser(): [User | null, (user: User) => void] { +export function useUser(): [User | null, (user: User | null) => void] { const { user, setUser } = useContext(SignedInUserContext)! return [user, setUser] } diff --git a/src/app/Fetcher.ts b/src/app/Fetcher.ts index 14b353e..55e8f44 100644 --- a/src/app/Fetcher.ts +++ b/src/app/Fetcher.ts @@ -68,11 +68,9 @@ export class Fetcher { } const nextToken = response.headers.get("Next-Authorization")! - const expirationDate = new Date( - response.headers.get("Next-Authorization-Expiration-Date")!, - ) + const expirationDate = response.headers.get("Next-Authorization-Expiration-Date")! if (nextToken && expirationDate) { - this.auth = { token: nextToken, expirationDate } + this.auth = { token: nextToken, expirationDate: new Date(expirationDate) } } return response diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 767caa6..0eff7a0 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -1,4 +1,4 @@ -import { FormEvent, useCallback, useRef, useState } from "react" +import { FormEvent, useCallback, useEffect, useRef, useState } from "react" import "../style/settings.css" import { useAppFetcher, useUser } from "../App.tsx" import { Fetcher } from "../app/Fetcher.ts" @@ -10,18 +10,19 @@ export default function ProfileSettings() { const [errorMessages, setErrorMessages] = useState([]) const [success, setSuccess] = useState(false) - const formRef = useRef(null) + 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() - const { name, email, password, confirmPassword } = - Object.fromEntries( - new FormData(formRef.current!) as Iterable< - [PropertyKey, string] - >, - ) + passwordConfirmRef.current!.checkValidity() if (password !== confirmPassword) { setErrorMessages(["Les mots de passe ne correspondent pas !"]) return @@ -46,9 +47,15 @@ export default function ProfileSettings() { formRef.current!.reset() setErrorMessages([]) }, - [fetcher, setUser, user], + [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 @@ -94,44 +101,64 @@ export default function ProfileSettings() { ref={formRef} id="credentials-form" onSubmit={submitForm}> -

Nom d'utilisateur

+ setName(e.target.value)} /> -

Addresse email

+ setEmail(e.target.value)} /> -

Mot de passe

+ setPassword(e.target.value)} /> -

Confirmez le mot de passe

+ setConfirmPassword(e.target.value)} /> @@ -173,11 +200,38 @@ function ProfileImageInputPopup({ show, onHide }: ProfileImageInputPopupProps) { 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 @@ -189,9 +243,9 @@ function ProfileImageInputPopup({ show, onHide }: ProfileImageInputPopupProps) { {msg}

))} -

+

+ setLink(e.target.value)} /> -
-
+ + ) } diff --git a/src/pages/template/Header.tsx b/src/pages/template/Header.tsx index 26401b4..9056ea8 100644 --- a/src/pages/template/Header.tsx +++ b/src/pages/template/Header.tsx @@ -11,7 +11,7 @@ export function Header() { {"profile-picture"} ) : ( 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 index 249770a..8c2f449 100644 --- a/src/style/settings.css +++ b/src/style/settings.css @@ -79,7 +79,7 @@ background-color: rgba(84, 78, 78, 0.33); } -#profile-picture-popup-content { +#profile-picture-popup-form { display: flex; flex-direction: column; row-gap: 20px; diff --git a/src/style/template/header.css b/src/style/template/header.css index ee48daf..6d64f46 100644 --- a/src/style/template/header.css +++ b/src/style/template/header.css @@ -40,12 +40,6 @@ color: var(--accent-color); } -#username { - text-align: center; - vertical-align: center; - margin: 0; -} - #clickable-header-right { border-radius: 1cap; -- 2.36.3