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 (