diff --git a/src/App.tsx b/src/App.tsx index d21929a..f1ab122 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,6 +23,7 @@ import { import { BASE } from "./Constants.ts" import { Authentication, Fetcher } from "./app/Fetcher.ts" import { User } from "./model/User.ts" +import { VisualizerPage } from "./pages/VisualizerPage.tsx" const HomePage = lazy(() => import("./pages/HomePage.tsx")) const LoginPage = lazy(() => import("./pages/LoginPage.tsx")) @@ -110,7 +111,6 @@ export default function App() { , )} /> - , )} /> - )} @@ -144,6 +143,21 @@ export default function App() { , )} /> + + + , + , + )} + /> + , + )} + /> (null) + const [courtType, setCourtType] = useState() + const [stepsTree, setStepsTree] = useState() + const fetcher = useAppFetcher() + const service = useMemo( + () => new APITacticService(fetcher, tacticId), + [tacticId], + ) + + const isNotInit = !stepsTree || !courtType + + useEffect(() => { + async function init() { + const contextResult = await service.getContext() + if (typeof contextResult === "string") { + setPanicMessage(contextResult) + return + } + + const rootStep = contextResult.stepsTree + setStepsTree(rootStep) + setCourtType(contextResult.courtType) + } + if (isNotInit) init() + }, [isNotInit, service]) + + if (panicMessage) { + return

{panicMessage}

+ } + if (isNotInit) { + return

Loading...

+ } + + return ( + + ) +} + +export interface StepVisualizerProps { + stepId?: number + stepsTree: StepInfoNode + courtType: CourtType + service: TacticService +} + +export function StepVisualizer({ + stepId, + stepsTree, + courtType, + service, +}: StepVisualizerProps) { + const [panicMessage, setPanicMessage] = useState(null) + const [content, setContent] = useState(null) + const [parentContent, setParentContent] = useState() + + const isNotInit = !content || !parentContent + + useEffect(() => { + async function init() { + const contentStepId = stepId ?? stepsTree.id + + const contentResult = await service.getContent(contentStepId) + if (typeof contentResult === "string") { + setPanicMessage(contentResult) + return + } + + const stepParent = getParent(stepsTree, contentStepId) + let parentContent = null + if (stepParent) { + const parentResult = await service.getContent(contentStepId) + if (typeof parentResult === "string") { + setPanicMessage(parentResult) + return + } + parentContent = parentResult + } + + setContent(contentResult) + setParentContent(parentContent) + } + + if (isNotInit) init() + }, [isNotInit, service, stepId, stepsTree]) + + if (panicMessage) { + return

{panicMessage}

+ } + + if (isNotInit) { + return

Loading Content...

+ } + + return ( + + ) +} + +export interface VisualizerFrameProps { + content: StepContent + parentContent: StepContent | null + courtType: CourtType +} + +export function VisualizerFrame({ + content, + parentContent, + courtType, +}: VisualizerFrameProps) { + const courtRef = useRef(null) + + const courtBounds = useCallback( + () => courtRef.current!.getBoundingClientRect(), + [courtRef], + ) + + const relativePositions = useMemo(() => { + const courtBounds = courtRef.current?.getBoundingClientRect() + return courtBounds + ? computeRelativePositions(courtBounds, content) + : new Map() + }, [content, courtRef]) + + const renderPlayer = useCallback( + (component: PlayerLike, isFromParent: boolean) => { + let info: PlayerInfo + const isPhantom = component.type == "phantom" + const usedContent = isFromParent ? parentContent! : content + + if (isPhantom) { + info = getPhantomInfo( + component, + usedContent, + relativePositions, + courtBounds(), + ) + } else { + info = component + } + + const className = + (isPhantom ? "phantom" : "player") + + " " + + (isFromParent ? "from-parent" : "") + + return ( + []} + /> + ) + }, + [content, courtBounds, parentContent, relativePositions], + ) + + const renderComponent = useCallback( + (component: TacticComponent, isFromParent: boolean): ReactNode => { + if (component.type === "player" || component.type === "phantom") { + return renderPlayer(component, isFromParent) + } + if (component.type === BALL_TYPE) { + return + } + return <> + }, + [renderPlayer], + ) + + const renderActions = useCallback( + (component: TacticComponent, isFromParent: boolean) => + component.actions.map((action, i) => { + return ( + + ) + }), + [courtRef], + ) + + return ( + } + courtRef={courtRef} + renderComponent={renderComponent} + renderActions={renderActions} + /> + ) +} diff --git a/src/components/editor/BasketCourt.tsx b/src/components/editor/BasketCourt.tsx index e3cddd2..903767f 100644 --- a/src/components/editor/BasketCourt.tsx +++ b/src/components/editor/BasketCourt.tsx @@ -1,13 +1,19 @@ -import { ReactElement, ReactNode, RefObject } from "react" +import { ReactElement, ReactNode, RefObject, useEffect, useState } from "react" import { Action } from "../../model/tactic/Action" import { CourtAction } from "./CourtAction" -import { ComponentId, TacticComponent } from "../../model/tactic/Tactic" +import { + ComponentId, + CourtType, + TacticComponent, +} from "../../model/tactic/Tactic" +import PlainCourt from "../../assets/court/full_court.svg?react" +import HalfCourt from "../../assets/court/half_court.svg?react" export interface BasketCourtProps { components: TacticComponent[] parentComponents: TacticComponent[] | null - previewAction: ActionPreview | null + previewAction?: ActionPreview | null renderComponent: (comp: TacticComponent, isFromParent: boolean) => ReactNode renderActions: (comp: TacticComponent, isFromParent: boolean) => ReactNode[] @@ -32,6 +38,13 @@ export function BasketCourt({ courtImage, courtRef, }: BasketCourtProps) { + const [court, setCourt] = useState(courtRef.current) + + //force update once the court reference is set + useEffect(() => { + setCourt(courtRef.current) + }, [courtRef]) + return (
{courtImage} - {courtRef.current && - parentComponents?.map((i) => renderComponent(i, true))} - {courtRef.current && - parentComponents?.flatMap((i) => renderActions(i, true))} + {court && parentComponents?.map((i) => renderComponent(i, true))} + {court && parentComponents?.flatMap((i) => renderActions(i, true))} - {courtRef.current && - components.map((i) => renderComponent(i, false))} - {courtRef.current && - components.flatMap((i) => renderActions(i, false))} + {court && components.map((i) => renderComponent(i, false))} + {court && components.flatMap((i) => renderActions(i, false))} {previewAction && ( ) } + +export function Court({ courtType }: { courtType: CourtType }) { + const CourtSvg = courtType === "PLAIN" ? PlainCourt : HalfCourt + return ( +
+ +
+ ) +} diff --git a/src/components/editor/CourtBall.tsx b/src/components/editor/CourtBall.tsx index 503f44c..4a60335 100644 --- a/src/components/editor/CourtBall.tsx +++ b/src/components/editor/CourtBall.tsx @@ -20,27 +20,6 @@ export function CourtBall({ }: EditableCourtBallProps) { const pieceRef = useRef(null) - function courtBallPiece( - { x, y }: Pos, - pieceRef?: RefObject, - onKeyUp?: KeyboardEventHandler, - ) { - return ( -
- -
- ) - } - if (ball.frozen) { return courtBallPiece(ball.pos) } @@ -58,3 +37,32 @@ export function CourtBall({ ) } + +interface CourtBallPieceProps { + pos: Pos +} + +export function CourtBallPiece({ pos }: CourtBallPieceProps) { + return courtBallPiece(pos) +} + +function courtBallPiece( + { x, y }: Pos, + pieceRef?: RefObject, + onKeyUp?: KeyboardEventHandler, +) { + return ( +
+ +
+ ) +} diff --git a/src/components/editor/StepsTree.tsx b/src/components/editor/StepsTree.tsx index dde6534..0029a5c 100644 --- a/src/components/editor/StepsTree.tsx +++ b/src/components/editor/StepsTree.tsx @@ -4,14 +4,14 @@ import BendableArrow from "../arrows/BendableArrow" import { ReactNode, useMemo, useRef } from "react" import AddSvg from "../../assets/icon/add.svg?react" import RemoveSvg from "../../assets/icon/remove.svg?react" -import { getStepName } from "../../editor/StepsDomain.ts" +import { getStepName } from "../../domains/StepsDomain.ts" export interface StepsTreeProps { root: StepInfoNode selectedStepId: number - onAddChildren: (parent: StepInfoNode) => void - onRemoveNode: (node: StepInfoNode) => void - onStepSelected: (node: StepInfoNode) => void + onAddChildren?: (parent: StepInfoNode) => void + onRemoveNode?: (node: StepInfoNode) => void + onStepSelected?: (node: StepInfoNode) => void } export default function StepsTree({ @@ -40,9 +40,9 @@ interface StepsTreeContentProps { rootNode: StepInfoNode selectedStepId: number - onAddChildren: (parent: StepInfoNode) => void - onRemoveNode: (node: StepInfoNode) => void - onStepSelected: (node: StepInfoNode) => void + onAddChildren?: (parent: StepInfoNode) => void + onRemoveNode?: (node: StepInfoNode) => void + onStepSelected?: (node: StepInfoNode) => void } function StepsTreeNode({ @@ -79,13 +79,17 @@ function StepsTreeNode({ onAddChildren(node)} + onAddButtonClicked={ + onAddChildren ? () => onAddChildren(node) : undefined + } onRemoveButtonClicked={ - rootNode.id === node.id + rootNode.id === node.id || !onRemoveNode ? undefined : () => onRemoveNode(node) } - onSelected={() => onStepSelected(node)}> + onSelected={() => { + if (onStepSelected) onStepSelected(node) + }}>

{useMemo( () => getStepName(rootNode, node.id), diff --git a/src/editor/ActionsDomains.ts b/src/domains/ActionsDomains.ts similarity index 98% rename from src/editor/ActionsDomains.ts rename to src/domains/ActionsDomains.ts index 8ad9e5a..2fa3229 100644 --- a/src/editor/ActionsDomains.ts +++ b/src/domains/ActionsDomains.ts @@ -3,16 +3,16 @@ import { Player, PlayerLike, PlayerPhantom, -} from "../model/tactic/Player" -import { ratioWithinBase } from "../geo/Pos" +} from "../model/tactic/Player.ts" +import { ratioWithinBase } from "../geo/Pos.ts" import { ComponentId, StepContent, TacticComponent, -} from "../model/tactic/Tactic" -import { overlaps } from "../geo/Box" -import { Action, ActionKind, moves } from "../model/tactic/Action" -import { removeBall, updateComponent } from "./TacticContentDomains" +} from "../model/tactic/Tactic.ts" +import { overlaps } from "../geo/Box.ts" +import { Action, ActionKind, moves } from "../model/tactic/Action.ts" +import { removeBall, updateComponent } from "./TacticContentDomains.ts" import { areInSamePath, getComponent, @@ -20,8 +20,8 @@ import { getPlayerNextTo, isNextInPath, removePlayer, -} from "./PlayerDomains" -import { BALL_TYPE } from "../model/tactic/CourtObjects" +} from "./PlayerDomains.ts" +import { BALL_TYPE } from "../model/tactic/CourtObjects.ts" export function getActionKind( target: TacticComponent | null, diff --git a/src/editor/PlayerDomains.ts b/src/domains/PlayerDomains.ts similarity index 88% rename from src/editor/PlayerDomains.ts rename to src/domains/PlayerDomains.ts index 2a7e28d..8c13c64 100644 --- a/src/editor/PlayerDomains.ts +++ b/src/domains/PlayerDomains.ts @@ -1,21 +1,22 @@ import { BallState, Player, + PlayerInfo, PlayerLike, PlayerPhantom, -} from "../model/tactic/Player" +} from "../model/tactic/Player.ts" import { ComponentId, StepContent, TacticComponent, -} from "../model/tactic/Tactic" +} from "../model/tactic/Tactic.ts" -import { removeComponent, updateComponent } from "./TacticContentDomains" +import { removeComponent, updateComponent } from "./TacticContentDomains.ts" import { removeAllActionsTargeting, spreadNewStateFromOriginStateChange, -} from "./ActionsDomains" -import { ActionKind } from "../model/tactic/Action" +} from "./ActionsDomains.ts" +import { ActionKind } from "../model/tactic/Action.ts" import { add, minus, @@ -311,7 +312,7 @@ export function truncatePlayerPath( for (let i = truncateStartIdx; i < path.items.length; i++) { const pathPhantomId = path.items[i] - //remove the phantom from the tactic + //remove the phantom from the domains content = removeComponent(pathPhantomId, content) content = removeAllActionsTargeting(pathPhantomId, content) } @@ -330,3 +331,46 @@ export function truncatePlayerPath( content, ) } + +export function getPhantomInfo( + phantom: PlayerPhantom, + content: StepContent, + relativePositions: ComputedRelativePositions, + courtBounds: DOMRect, +): PlayerInfo { + const origin = getOrigin(phantom, content.components) + + return { + id: phantom.id, + team: origin.team, + role: origin.role, + pos: computePhantomPositioning( + phantom, + content, + relativePositions, + courtBounds, + ), + ballState: phantom.ballState, + } +} + +export type ComputedRelativePositions = Map + +export function computeRelativePositions( + courtBounds: DOMRect, + content: StepContent, +) { + const relativePositionsCache: ComputedRelativePositions = new Map() + + for (const component of content.components) { + if (component.type !== "phantom") continue + computePhantomPositioning( + component, + content, + relativePositionsCache, + courtBounds, + ) + } + + return relativePositionsCache +} diff --git a/src/editor/StepsDomain.ts b/src/domains/StepsDomain.ts similarity index 97% rename from src/editor/StepsDomain.ts rename to src/domains/StepsDomain.ts index a2b739a..a120c23 100644 --- a/src/editor/StepsDomain.ts +++ b/src/domains/StepsDomain.ts @@ -1,4 +1,4 @@ -import { StepInfoNode } from "../model/tactic/Tactic" +import { StepInfoNode } from "../model/tactic/Tactic.ts" export function addStepNode( root: StepInfoNode, diff --git a/src/editor/TacticContentDomains.ts b/src/domains/TacticContentDomains.ts similarity index 93% rename from src/editor/TacticContentDomains.ts rename to src/domains/TacticContentDomains.ts index c7b2965..bd256c7 100644 --- a/src/editor/TacticContentDomains.ts +++ b/src/domains/TacticContentDomains.ts @@ -1,4 +1,4 @@ -import { equals, Pos, ratioWithinBase } from "../geo/Pos" +import { equals, Pos, ratioWithinBase } from "../geo/Pos.ts" import { BallState, @@ -7,28 +7,28 @@ import { PlayerLike, PlayerPhantom, PlayerTeam, -} from "../model/tactic/Player" +} from "../model/tactic/Player.ts" import { Ball, BALL_ID, BALL_TYPE, CourtObject, -} from "../model/tactic/CourtObjects" +} from "../model/tactic/CourtObjects.ts" import { ComponentId, StepContent, TacticComponent, -} from "../model/tactic/Tactic" +} from "../model/tactic/Tactic.ts" -import { overlaps } from "../geo/Box" -import { RackedCourtObject, RackedPlayer } from "./RackedItems" +import { overlaps } from "../geo/Box.ts" +import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems.ts" import { getComponent, getOrigin, getPrecomputedPosition, removePlayer, tryGetComponent, -} from "./PlayerDomains" +} from "./PlayerDomains.ts" import { Action, ActionKind } from "../model/tactic/Action.ts" import { spreadNewStateFromOriginStateChange } from "./ActionsDomains.ts" @@ -418,6 +418,21 @@ export function drainTerminalStateOnChildContent( continue } + if (childComponent.type !== parentComponent.type) + throw Error("child and parent components are not of the same type.") + + if (childComponent.type === "ball" && parentComponent.type === "ball") { + gotUpdated = true + childContent = updateComponent( + { + ...childComponent, + frozen: true, + pos: parentComponent.pos, + }, + childContent, + ) + } + // ensure that the component is a player if ( parentComponent.type !== "player" || @@ -439,7 +454,7 @@ export function drainTerminalStateOnChildContent( newContentResult?.components, ) } - // update the position of the player if it has been moved + // update the position of the component if it has been moved // also force update if the child component is not frozen (the component was introduced previously by the child step but the parent added it afterward) if ( !childComponent.frozen || @@ -459,6 +474,7 @@ export function drainTerminalStateOnChildContent( const initialChildCompsCount = childContent.components.length + //remove players if they are not present on the parent's anymore for (const component of childContent.components) { if ( component.type !== "phantom" && diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index f86db7c..7a9a929 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -1,5 +1,6 @@ import { CSSProperties, + ReactNode, RefObject, SetStateAction, useCallback, @@ -10,8 +11,6 @@ import { } from "react" import "../style/editor.css" import TitleInput from "../components/TitleInput" -import PlainCourt from "../assets/court/full_court.svg?react" -import HalfCourt from "../assets/court/half_court.svg?react" import { BallPiece } from "../components/editor/BallPiece" @@ -19,7 +18,6 @@ import { Rack } from "../components/Rack" import { PlayerPiece } from "../components/editor/PlayerPiece" import { - ComponentId, CourtType, StepContent, StepInfoNode, @@ -33,7 +31,11 @@ import SavingState, { import { BALL_TYPE } from "../model/tactic/CourtObjects" import { CourtAction } from "../components/editor/CourtAction" -import { ActionPreview, BasketCourt } from "../components/editor/BasketCourt" +import { + ActionPreview, + BasketCourt, + Court, +} from "../components/editor/BasketCourt" import { overlaps } from "../geo/Box" import { @@ -50,7 +52,7 @@ import { removeBall, selectContent, updateComponent, -} from "../editor/TacticContentDomains" +} from "../domains/TacticContentDomains.ts" import { BallState, @@ -71,16 +73,18 @@ import { isActionValid, removeAction, spreadNewStateFromOriginStateChange, -} from "../editor/ActionsDomains" +} from "../domains/ActionsDomains.ts" 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, + ComputedRelativePositions, + computeRelativePositions, getOrigin, + getPhantomInfo, removePlayer, -} from "../editor/PlayerDomains" +} from "../domains/PlayerDomains.ts" import { CourtBall } from "../components/editor/CourtBall" import StepsTree from "../components/editor/StepsTree" import { @@ -88,12 +92,15 @@ import { getParent, getStepNode, removeStepNode, -} from "../editor/StepsDomain" +} from "../domains/StepsDomain.ts" import SplitLayout from "../components/SplitLayout.tsx" -import { ServiceError, TacticService } from "../service/TacticService.ts" +import { + MutableTacticService, + ServiceError, +} from "../service/MutableTacticService.ts" import { LocalStorageTacticService } from "../service/LocalStorageTacticService.ts" import { APITacticService } from "../service/APITacticService.ts" -import { useParams } from "react-router-dom" +import { useNavigate, useParams } from "react-router-dom" import { ContentVersions } from "../editor/ContentVersions.ts" import { useAppFetcher } from "../App.tsx" @@ -101,21 +108,15 @@ const ERROR_STYLE: CSSProperties = { borderColor: "red", } -type ComputedRelativePositions = Map - type ComputedStepContent = { content: StepContent relativePositions: ComputedRelativePositions } -export interface EditorPageProps { +export interface EditorProps { guestMode: boolean } -export default function Editor({ guestMode }: EditorPageProps) { - return -} - interface EditorService { addStep( parent: StepInfoNode, @@ -129,28 +130,47 @@ interface EditorService { setContent(content: SetStateAction): void setName(name: string): Promise + + openVisualizer(): Promise } -function EditorPortal({ guestMode }: EditorPageProps) { +export default function Editor({ guestMode }: EditorProps) { const { tacticId: idStr } = useParams() const fetcher = useAppFetcher() + + const navigate = useNavigate() if (guestMode || !idStr) { - return + return ( + navigate("/tactic/view-guest")} + /> + ) } return ( navigate(`/tactic/${idStr}/view`)} /> ) } -function EditorPageWrapper({ service }: { service: TacticService }) { +interface EditorPageWrapperProps { + service: MutableTacticService + openVisualizer(): void +} + +function EditorPageWrapper({ + service, + openVisualizer, +}: EditorPageWrapperProps) { const [panicMessage, setPanicMessage] = useState() const [stepId, setStepId] = useState() const [tacticName, setTacticName] = useState() const [courtType, setCourtType] = useState() const [stepsTree, setStepsTree] = useState() + const [parentContent, setParentContent] = useState(null) const courtRef = useRef(null) @@ -209,8 +229,6 @@ function EditorPageWrapper({ service }: { service: TacticService }) { [stepsVersions, service, stepId, stepsTree], ) - const [parentContent, setParentContent] = useState(null) - const [stepContent, setStepContent, saveState] = useContentState( { components: [] }, @@ -274,7 +292,6 @@ function EditorPageWrapper({ service }: { service: TacticService }) { stepsVersions.set(stepId, versions) versions.insertAndCut(contentResult) - console.log(contentResult) setStepContent(contentResult, false) } @@ -334,8 +351,12 @@ function EditorPageWrapper({ service }: { service: TacticService }) { setStepId(step) setStepContent(result, false) }, + + async openVisualizer(): Promise { + openVisualizer() + }, } - }, [stepsVersions, service, setStepContent, stepsTree]) + }, [stepsTree, service, stepsVersions, setStepContent, openVisualizer]) if (panicMessage) { return

{panicMessage}

@@ -399,7 +420,7 @@ function EditorPage({ null, ) - const [isStepsTreeVisible, setStepsTreeVisible] = useState(false) + const [isStepsTreeVisible, setStepsTreeVisible] = useState(true) const courtBounds = useCallback( () => courtRef.current!.getBoundingClientRect(), @@ -543,6 +564,7 @@ function EditorPage({ content, courtRef, doMoveBall, + parentContent, previewAction?.isInvalid, service.setContent, ], @@ -558,22 +580,14 @@ function EditorPage({ const usedContent = isFromParent ? parentContent! : content if (isPhantom) { - const origin = getOrigin(component, usedContent.components) - info = { - id: component.id, - team: origin.team, - role: origin.role, - pos: computePhantomPositioning( - component, - usedContent, - relativePositions, - courtBounds(), - ), - ballState: component.ballState, - } + info = getPhantomInfo( + component, + usedContent, + relativePositions, + courtBounds(), + ) } else { info = component - forceFreeze ||= component.frozen } @@ -612,8 +626,9 @@ function EditorPage({ ) }, [ - courtRef, + parentContent, content, + courtRef, relativePositions, courtBounds, renderAvailablePlayerActions, @@ -649,7 +664,7 @@ function EditorPage({ ) const renderComponent = useCallback( - (component: TacticComponent, isFromParent: boolean) => { + (component: TacticComponent, isFromParent: boolean): ReactNode => { if (component.type === "player" || component.type === "phantom") { return renderPlayer(component, isFromParent) } @@ -669,7 +684,7 @@ function EditorPage({ /> ) } - throw new Error("unknown tactic component " + component) + return <> }, [service, renderPlayer, doMoveBall], ) @@ -799,25 +814,26 @@ function EditorPage({
-
- { - service.setName(new_name).then((state) => { - setTitleStyle( - state == SaveStates.Ok - ? {} - : ERROR_STYLE, - ) - }) - }, - [service], - )} - /> -
+ { + service.setName(new_name).then((state) => { + setTitleStyle( + state == SaveStates.Ok ? {} : ERROR_STYLE, + ) + }) + }, + [service], + )} + />
+ @@ -207,25 +207,28 @@ function ProfileImageInputPopup({ show, onHide }: ProfileImageInputPopupProps) { if (e.key === "Escape") onHide() } - window.addEventListener('keyup', onKeyUp) - return () => window.removeEventListener('keyup', onKeyUp) + window.addEventListener("keyup", onKeyUp) + return () => window.removeEventListener("keyup", onKeyUp) }, [onHide]) - const handleForm = useCallback(async (e: FormEvent) => { - e.preventDefault() + 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]) + 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 <> @@ -243,7 +246,9 @@ function ProfileImageInputPopup({ show, onHide }: ProfileImageInputPopupProps) { {msg}
))} -