From 3cf82e6739e57bfb9e46961f818f3420daaa9794 Mon Sep 17 00:00:00 2001 From: Eloi Berlinger <114403558+eloiberlinger1@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:06:57 +0200 Subject: [PATCH] Revert "Fix linter errors" --- frontend/src/features/editor/EditorApp.tsx | 38 ++++--- .../editor/components/AddHoldModal.tsx | 29 +++-- .../editor/components/FileManager.tsx | 12 +- .../editor/components/HoldInspector.tsx | 8 +- .../features/editor/components/MainCanvas.tsx | 104 +++++++++++------- .../editor/components/ModelViewer.tsx | 12 +- .../features/editor/components/Sidebar.tsx | 35 +++--- .../editor/components/SidebarHoldsSection.tsx | 60 +++++----- .../editor/components/useDragPreview.ts | 12 +- frontend/src/features/editor/store.ts | 41 ------- .../src/features/editor/stubs/Hold360.tsx | 7 +- .../features/editor/utils/HandleAddHold.tsx | 26 +++-- .../editor/utils/HandleLoadSession.tsx | 11 +- .../utils/useHoldAvailabilityValidation.tsx | 30 ++--- frontend/src/shared/auth/AuthProvider.tsx | 1 - 15 files changed, 204 insertions(+), 222 deletions(-) diff --git a/frontend/src/features/editor/EditorApp.tsx b/frontend/src/features/editor/EditorApp.tsx index 6f2991a..3e98e88 100644 --- a/frontend/src/features/editor/EditorApp.tsx +++ b/frontend/src/features/editor/EditorApp.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useGLTF } from "@react-three/drei"; import { useParams } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; @@ -6,7 +6,6 @@ import { posthog } from "@/shared/analytics/posthog"; import useWallSessionQuery from "./utils/WallSessionQuery"; import { usePlacementStore } from "./store"; -import type { SessionHoldInstance } from "./store"; import { useHandleLoadSession } from "./utils/HandleLoadSession"; import MainCanvas from "./components/MainCanvas"; @@ -16,6 +15,15 @@ import FileManager from "./components/FileManager"; import { useTranslation } from "react-i18next"; import Tutorial from "./components/Tutorial"; +interface HoldInstance { + id: string; + hold_instance_id?: string; + hold_type?: { + glb_url?: string; + }; + [key: string]: unknown; +} + const transformTools = [ { id: "translate", icon: "open_with", label: "Translate", hint: "Shift + Left click" }, { id: "rotate", icon: "sync", label: "Rotate", hint: "Left click" }, @@ -76,25 +84,23 @@ function EditorApp() { return () => window.removeEventListener("beforeunload", handleBeforeUnload); }, [hasUnsavedChanges, t]); + const holdModels: Array> = []; + const holdModelsGLBURL: string[] = []; + useEffect(() => { const glbUrl = session_data?.related_wall?.glb_url; setWallModels(glbUrl ? [glbUrl] : []); }, [session_data?.related_wall?.glb_url]); - const { holdModels, holdModelsGLBURL } = useMemo(() => { - const holdModels: SessionHoldInstance[] = []; - const holdModelsGLBURL: string[] = []; - if (session_data?.related_holds_collection) { - session_data.holds_collection_instances?.forEach((hold: SessionHoldInstance) => { - hold.hold_instance_id = hold.id; - holdModels.push(hold); - if (hold.hold_type?.glb_url) { - holdModelsGLBURL.push(hold.hold_type.glb_url); - } - }); - } - return { holdModels, holdModelsGLBURL }; - }, [session_data?.related_holds_collection, session_data?.holds_collection_instances]); + if (session_data?.related_holds_collection) { + session_data.holds_collection_instances?.forEach((hold: any) => { + hold.hold_instance_id = hold.id; + holdModels.push(hold); + if (hold.hold_type?.glb_url) { + holdModelsGLBURL.push(hold.hold_type.glb_url); + } + }); + } const { preload } = useGLTF; useEffect(() => { diff --git a/frontend/src/features/editor/components/AddHoldModal.tsx b/frontend/src/features/editor/components/AddHoldModal.tsx index 1538324..8d5d974 100644 --- a/frontend/src/features/editor/components/AddHoldModal.tsx +++ b/frontend/src/features/editor/components/AddHoldModal.tsx @@ -5,19 +5,19 @@ import HandleAddHold from "../utils/HandleAddHold"; import { useEditorAuth } from "../mocks/useEditorAuth"; import PaginationList from "../stubs/PaginationList"; import { useTranslation } from "react-i18next"; -import type { HoldModel, SessionData } from "../store"; -type StockItem = { - id: string | number; - hold_type: { id: string | number; cdn_ref?: string; [key: string]: unknown }; - model?: string; - [key: string]: unknown; +type HoldModel = { + name: string; + file: string; + hold_type: Record; + hold_instance_id: string; + id?: string; }; interface AddHoldModalProps { isOpen: boolean; onClose: () => void; - session_data: SessionData; + session_data: any; onHoldAdded: (hold: HoldModel) => void; currentHolds: HoldModel[]; } @@ -32,7 +32,7 @@ export default function AddHoldModal({ const [search, setSearch] = useState(""); const [isAdding, setIsAdding] = useState(false); const scrollRef = useRef(null); - const [addedHoldIds, setAddedHoldIds] = useState>(new Set()); + const [addedHoldIds, setAddedHoldIds] = useState>(new Set()); const { user, authenticatedFetch } = useEditorAuth(); const [page, setPage] = useState(1); const API_URL = import.meta.env.VITE_API_BASE; @@ -97,7 +97,7 @@ export default function AddHoldModal({ ); const deduplicatedStockData = Array.isArray(rawStockData) - ? rawStockData.reduce((acc: StockItem[], current: StockItem) => { + ? rawStockData.reduce((acc: any[], current: any) => { const exists = acc.find( (hold) => hold.hold_type.id === current.hold_type.id ); @@ -108,25 +108,24 @@ export default function AddHoldModal({ const stockData = Array.isArray(deduplicatedStockData) ? deduplicatedStockData.filter( - (hold: StockItem) => + (hold: any) => !currentHoldTypeIds.has(hold.hold_type.id) && - !addedHoldIds.has(hold.hold_type.id as number) + !addedHoldIds.has(hold.hold_type.id) ) : deduplicatedStockData; const filteredStockData = Array.isArray(stockData) - ? stockData.filter((hold: StockItem) => { + ? stockData.filter((hold: any) => { if (!search) return true; const searchLower = search.toLowerCase(); - const manufacturerName = hold.hold_type?.manufacturer_name as string | undefined; return ( hold.model?.toLowerCase().includes(searchLower) || - manufacturerName?.toLowerCase().includes(searchLower) + hold.hold_type?.manufacturer_name?.toLowerCase().includes(searchLower) ); }) : stockData; - const handleAddHoldClick = async (hold: StockItem) => { + const handleAddHoldClick = async (hold: any) => { setIsAdding(true); try { const result = await HandleAddHold(hold, session_data, onHoldAdded); diff --git a/frontend/src/features/editor/components/FileManager.tsx b/frontend/src/features/editor/components/FileManager.tsx index f5aac26..bfeb3b8 100644 --- a/frontend/src/features/editor/components/FileManager.tsx +++ b/frontend/src/features/editor/components/FileManager.tsx @@ -1,12 +1,11 @@ import { useState } from "react"; import { usePlacementStore } from "../store"; -import type { SessionData } from "../store"; import { useNavigate } from "react-router-dom"; import { useEditorAuth } from "../mocks/useEditorAuth"; import { useTranslation } from "react-i18next"; import { posthog } from "@/shared/analytics/posthog"; -const FileManager = ({ session_data }: { session_data: SessionData }) => { +const FileManager = ({ session_data }: { session_data: any }) => { const objects = usePlacementStore((s) => s.objects); const wallColors = usePlacementStore((s) => s.wallColors); const holdColors = usePlacementStore((s) => s.holdColors); @@ -17,16 +16,15 @@ const FileManager = ({ session_data }: { session_data: SessionData }) => { const navigate = useNavigate(); const { t } = useTranslation(); - const cleanObjectsForSave = (objs: typeof objects, data: SessionData) => { + const cleanObjectsForSave = (objs: typeof objects, data: any) => { return objs.map((obj) => { - const cleanedObj: Record = { ...obj }; - const url = cleanedObj.url as string | undefined; - if (cleanedObj.type === "wall" && url?.startsWith("blob:")) { + const cleanedObj = { ...obj } as any; + if (cleanedObj.type === "wall" && cleanedObj.url?.startsWith("blob:")) { if (data?.related_wall?.id) { cleanedObj.wall_id = data.related_wall.id; } delete cleanedObj.url; - } else if (url?.startsWith("blob:")) { + } else if (cleanedObj.url?.startsWith("blob:")) { delete cleanedObj.url; } return cleanedObj; diff --git a/frontend/src/features/editor/components/HoldInspector.tsx b/frontend/src/features/editor/components/HoldInspector.tsx index c7cbb53..9eafdd2 100644 --- a/frontend/src/features/editor/components/HoldInspector.tsx +++ b/frontend/src/features/editor/components/HoldInspector.tsx @@ -17,8 +17,8 @@ function RotationHandle({ const handleY = radius + handleRadius * Math.sin(rotation - Math.PI / 2); const onPointerDown = (e: React.PointerEvent) => { e.preventDefault(); - window.addEventListener("pointermove", onPointerMove as EventListener); - window.addEventListener("pointerup", onPointerUp as EventListener); + window.addEventListener("pointermove", onPointerMove as any); + window.addEventListener("pointerup", onPointerUp as any); }; const onPointerMove = (e: PointerEvent) => { if (!circleRef.current) return; @@ -29,8 +29,8 @@ function RotationHandle({ onRotate(angle); }; const onPointerUp = () => { - window.removeEventListener("pointermove", onPointerMove as EventListener); - window.removeEventListener("pointerup", onPointerUp as EventListener); + window.removeEventListener("pointermove", onPointerMove as any); + window.removeEventListener("pointerup", onPointerUp as any); }; return (
{ if (model && pos && quat && alignedQuat) { let customRotation = 0; - if (typeof model.customRotation === "number") { - customRotation = model.customRotation; + if (typeof (model as any).customRotation === "number") { + customRotation = (model as any).customRotation; } else if (model.type === "hold") { customRotation = 0; } @@ -84,7 +85,7 @@ function DragPreview() { const parentObj = objects.find((o) => o.id === parentId); if (parentObj) { // Combine base rotation and customRotation for parent - const parentQ = new THREE.Quaternion(...parentObj.rotation); + let parentQ = new THREE.Quaternion(...parentObj.rotation); if (typeof parentObj.customRotation === "number") { const customQ = new THREE.Quaternion(); customQ.setFromAxisAngle( @@ -111,7 +112,7 @@ function DragPreview() { const match = model.url.match(/\/([^\/]+)\.[^.]+$/); holdName = match ? match[1] : model.url; } - const newId = model.id || uuidv4(); + let newId = model.id || uuidv4(); addObject({ id: newId, type: model.type, @@ -158,10 +159,6 @@ function DragPreview() { wallColors, holdColors, coloredTexture, - addObject, - endDrag, - selectObject, - selectedObjId, ]); if (!dragging || !model || !model.url) return null; @@ -207,27 +204,38 @@ function PlacedObjects({ const dragging = useDragStore((s) => s.dragging); const { camera, scene, gl } = useThree(); const [draggingId, setDraggingId] = useState(null); + const [draggedObj, setDraggedObj] = useState(null); + const [draggedChildren, setDraggedChildren] = useState([]); // Ref to track which hold received pointer down const pointerDownHoldIdRef = React.useRef(null); - const draggedObj = useMemo( - () => draggingId ? (objects.find((o) => o.id === draggingId) ?? null) : null, - [draggingId, objects] - ); - const draggedChildren = useMemo( - () => draggedObj ? objects.filter((o) => o.parentId === draggedObj.id).map((o) => o.id) : [], - [draggedObj, objects] - ); - // Find if any hold is selected - const selectedHold = useMemo(() => + const selectedHold = useMemo(() => objects.find((o) => o.type === "hold" && o.id === selectedObjId), [objects, selectedObjId] ); useEffect(() => { if (onHoldDragState) onHoldDragState(!!selectedHold); - }, [selectedHold, onHoldDragState]); + }, [selectedHold]); + + // Set dragged object and its children on drag start + useEffect(() => { + if (draggingId) { + const obj = objects.find((o) => o.id === draggingId) || null; + setDraggedObj(obj); + // Store children ids if dragging a parent + if (obj) { + const children = objects + .filter((o) => o.parentId === obj.id) + .map((o) => o.id); + setDraggedChildren(children); + } + } else { + setDraggedObj(null); + setDraggedChildren([]); + } + }, [draggingId, objects]); // Use unified drag preview logic const { pos: previewPos, quat: previewQuat } = useDragPreview({ @@ -249,6 +257,7 @@ function PlacedObjects({ }); } setDraggingId(null); + setDraggedObj(null); document.body.style.cursor = "grab"; window.removeEventListener("mouseup", handleUp); window.removeEventListener("touchend", handleUp); @@ -274,15 +283,18 @@ function PlacedObjects({ updateObject(childId, { parentId: newParent.id }); }); } + setDraggedChildren([]); } }, [dragging, draggedObj, draggedChildren, objects, updateObject]); // Helper: recursively render hold tree function renderHoldTree(parentId: string | null) { return objects - .filter((o) => o.type === "hold" && o.parentId === parentId && o.url) + .filter((o) => o.type === "hold" && o.parentId === parentId && o.url) // Only render objects with valid URLs .map((obj) => { + // Hide children during parent re-drag if (draggingId && draggingId === obj.id) return null; + // Compose rotation with customRotation if present let groupQuaternion = obj.rotation; if (typeof obj.customRotation === "number") { const baseQ = new THREE.Quaternion(...obj.rotation); @@ -294,12 +306,13 @@ function PlacedObjects({ baseQ.multiply(customQ); groupQuaternion = [baseQ.x, baseQ.y, baseQ.z, baseQ.w]; } + // Timer-based click vs drag logic let dragTimer: number | null = null; let dragStarted = false; const handlePointerDown = (e: React.PointerEvent) => { e.stopPropagation(); dragStarted = false; - pointerDownHoldIdRef.current = obj.id; + pointerDownHoldIdRef.current = obj.id; // Track pointer down hold dragTimer = window.setTimeout(() => { dragStarted = true; removeObject(obj.id); @@ -320,6 +333,7 @@ function PlacedObjects({ clearTimeout(dragTimer); dragTimer = null; } + // Only unselect if pointer down and up happened on the same hold if (!dragStarted && pointerDownHoldIdRef.current === obj.id) { if (selectedObjId === obj.id) { selectObject(null); @@ -412,40 +426,48 @@ function PlacedObjects({ } const MainCanvas = ({ wallModels }: { wallModels: string[] }) => { + const [isWallLoading, setIsWallLoading] = useState(true); const [wallLoadCount, setWallLoadCount] = useState(0); - const [wallBatchSize, setWallBatchSize] = useState(0); - const [forceLoaded, setForceLoaded] = useState(false); + // Add wall by default using the first element from wallModels const addObject = usePlacementStore((s) => s.addObject); const updateObject = usePlacementStore((s) => s.updateObject); const objects = usePlacementStore((s) => s.objects); - const wallObjects = useMemo(() => - objects.filter((o) => o.type === "wall"), + // Count wall objects to track loading + const wallObjects = useMemo(() => + objects.filter((o) => o.type === "wall"), [objects] ); - - // Sync batch size with current wall count — React's recommended pattern for resetting - // state when derived data changes (avoids setState-in-effect anti-pattern). - if (wallBatchSize !== wallObjects.length) { - setWallBatchSize(wallObjects.length); - setWallLoadCount(0); - setForceLoaded(false); - } - - const isWallLoading = !forceLoaded && wallObjects.length > 0 && wallLoadCount < wallObjects.length; - + + useEffect(() => { + if (wallObjects.length > 0) { + setWallLoadCount(0); // Reset counter when walls change + setIsWallLoading(true); + } + }, [wallObjects.length]); + const handleWallLoadComplete = useCallback(() => { setWallLoadCount(prev => prev + 1); }, []); - - // Fallback: if onLoadComplete never fires (e.g. model cached), stop loading after 1s + + useEffect(() => { + if (wallLoadCount >= wallObjects.length && wallObjects.length > 0) { + setIsWallLoading(false); + } + }, [wallLoadCount, wallObjects.length]); + + // If walls are loaded from session but haven't triggered load callbacks, mark as loaded after a delay useEffect(() => { if (wallObjects.length > 0 && isWallLoading) { - const timer = setTimeout(() => setForceLoaded(true), 1000); + const timer = setTimeout(() => { + setIsWallLoading(false); + }, 1000); // Give a short delay for walls that were loaded from session return () => clearTimeout(timer); } }, [wallObjects.length, isWallLoading]); + // Use a ref to track if we've already added a wall to avoid infinite loop + // Reset when wallModels change (new session) const wallAddedRef = useRef(null); useEffect(() => { @@ -491,11 +513,13 @@ const MainCanvas = ({ wallModels }: { wallModels: string[] }) => { console.log("[MainCanvas] Wall already exists, skipping add"); } wallAddedRef.current = currentWallUrl; + setIsWallLoading(false); } } else { console.log("[MainCanvas] No wallModels, clearing wall"); - // No wall available — clear ref + // No wall available — clear ref and stop loading overlay wallAddedRef.current = null; + setIsWallLoading(false); } }, [wallModels, addObject, updateObject, objects]); diff --git a/frontend/src/features/editor/components/ModelViewer.tsx b/frontend/src/features/editor/components/ModelViewer.tsx index 7be6bad..a782a6d 100644 --- a/frontend/src/features/editor/components/ModelViewer.tsx +++ b/frontend/src/features/editor/components/ModelViewer.tsx @@ -101,17 +101,13 @@ const ModelViewerInner = ({ let texture: THREE.Texture | null = null; if (Array.isArray(child.material)) { for (const mat of child.material) { - const stdMat = mat as THREE.MeshStandardMaterial; - if (stdMat.map) { - texture = stdMat.map; + if ((mat as any).map) { + texture = (mat as any).map; break; } } - } else { - const stdMat = child.material as THREE.MeshStandardMaterial; - if (stdMat.map) { - texture = stdMat.map; - } + } else if ((child.material as any).map) { + texture = (child.material as any).map; } if (texture) { diff --git a/frontend/src/features/editor/components/Sidebar.tsx b/frontend/src/features/editor/components/Sidebar.tsx index 881ec33..794a620 100644 --- a/frontend/src/features/editor/components/Sidebar.tsx +++ b/frontend/src/features/editor/components/Sidebar.tsx @@ -2,7 +2,14 @@ import SidebarHoldsSection from "./SidebarHoldsSection"; import AddHoldModal from "./AddHoldModal"; import { useState, useRef } from "react"; import { useTranslation } from "react-i18next"; -import type { HoldModel, SessionData, SessionHoldInstance } from "../store"; + +type HoldModel = { + name: string; + file: string; + hold_type: Record; + hold_instance_id: string; + id?: string; +}; const Sidebar = ({ wallModels: _wallModels, @@ -10,20 +17,18 @@ const Sidebar = ({ session_data, }: { wallModels: string[]; - holdModels: SessionHoldInstance[]; - session_data: SessionData; + holdModels: Array>; + session_data: any; }) => { const { t } = useTranslation(); - const processedHoldModels: HoldModel[] = holdModels - .filter((hold) => !!hold.hold_type) - .map((hold) => ({ - name: (hold.hold_type!.manufacturer_ref as string | undefined) ?? '', - file: (hold.hold_type!.cdn_ref as string | undefined) ?? '', - hold_type: hold.hold_type!, - hold_instance_id: hold.id, - id: hold.id, - })); + const processedHoldModels: HoldModel[] = holdModels.map((hold) => ({ + name: hold.hold_type.manufacturer_ref, + file: hold.hold_type.cdn_ref, + hold_type: hold.hold_type, + hold_instance_id: hold.id, + id: hold.id, + })); const holdsSectionRef = useRef<{ addHold: (hold: HoldModel) => void; @@ -31,10 +36,8 @@ const Sidebar = ({ }>(null); const [addHoldModalOpen, setAddHoldModalOpen] = useState(false); - const [locallyAddedHolds, setLocallyAddedHolds] = useState([]); const handleHoldAddedFromModal = (newHold: HoldModel) => { - setLocallyAddedHolds((prev) => [...prev, newHold]); if (holdsSectionRef.current) { holdsSectionRef.current.addHold(newHold); } @@ -55,7 +58,9 @@ const Sidebar = ({ onClose={() => setAddHoldModalOpen(false)} session_data={session_data} onHoldAdded={handleHoldAddedFromModal} - currentHolds={[...processedHoldModels, ...locallyAddedHolds]} + currentHolds={ + holdsSectionRef.current?.getCurrentHolds() || processedHoldModels + } />
diff --git a/frontend/src/features/editor/components/SidebarHoldsSection.tsx b/frontend/src/features/editor/components/SidebarHoldsSection.tsx index 5e74d43..63e487b 100644 --- a/frontend/src/features/editor/components/SidebarHoldsSection.tsx +++ b/frontend/src/features/editor/components/SidebarHoldsSection.tsx @@ -2,12 +2,10 @@ import { useState, forwardRef, useImperativeHandle, + useEffect, useRef, - useMemo, - useCallback, } from "react"; import { useDragStore, usePlacementStore } from "../store"; -import type { HoldModel, SessionData } from "../store"; import Hold360, { HoldScrollContext } from "../stubs/Hold360"; import React from "react"; import { useEditorAuth } from "../mocks/useEditorAuth"; @@ -17,6 +15,14 @@ import { useGLTF } from "@react-three/drei"; import { posthog } from "@/shared/analytics/posthog"; import HoldInfoModal from "./HoldInfoModal"; +type HoldModel = { + name: string; + file: string; + hold_type: Record; + hold_instance_id: string; + id?: string; +}; + interface SidebarHoldsSectionRef { addHold: (hold: HoldModel) => void; getCurrentHolds: () => HoldModel[]; @@ -26,26 +32,27 @@ const SidebarHoldsSection = forwardRef< SidebarHoldsSectionRef, { holdModels: HoldModel[]; - session_data: SessionData; + session_data: any; } >(({ holdModels, session_data }, ref) => { - const [locallyAddedHolds, setLocallyAddedHolds] = useState([]); - const [deletedHoldTypeIds, setDeletedHoldTypeIds] = useState>(new Set()); + const [models, setModels] = useState(holdModels); const [showHolds, setShowHolds] = useState(true); const [showVolumes, setShowVolumes] = useState(true); const scrollRef = useRef(null); const [infoHold, setInfoHold] = useState(null); - const models = useMemo(() => { - const extraHolds = locallyAddedHolds.filter( - (local) => !holdModels.some( - (server) => server.hold_instance_id === local.hold_instance_id - ) - ); - return [...holdModels, ...extraHolds].filter( - (m) => !deletedHoldTypeIds.has(String(m.hold_type?.id)) - ); - }, [holdModels, locallyAddedHolds, deletedHoldTypeIds]); + useEffect(() => { + setModels((prevModels) => { + const locallyAddedHolds = prevModels.filter( + (prevModel) => + !holdModels.some( + (holdModel) => + holdModel.hold_instance_id === prevModel.hold_instance_id + ) + ); + return [...holdModels, ...locallyAddedHolds]; + }); + }, [holdModels]); const startDrag = useDragStore((s) => s.startDrag); const endDrag = useDragStore((s) => s.endDrag); @@ -54,7 +61,7 @@ const SidebarHoldsSection = forwardRef< const { t } = useTranslation(); const { user, authenticatedFetch } = useEditorAuth(); const API_URL = import.meta.env.VITE_API_BASE; - const [, setCurrentDownloadUrl] = useState(); + const [_currentDownloadUrl, setCurrentDownloadUrl] = useState(); const { data: stockData } = useQuery({ queryKey: ["stocks", user?.related_gym_id], @@ -66,14 +73,14 @@ const SidebarHoldsSection = forwardRef< enabled: !!user && !!user?.related_gym_id, }); - const addHoldToLocal = useCallback((newHold: HoldModel) => { - setLocallyAddedHolds((prev) => [...prev, newHold]); + const addHoldToLocal = (newHold: HoldModel) => { + setModels((prev) => [...prev, newHold]); if (newHold.hold_type?.glb_url) { useGLTF.preload(newHold.hold_type.glb_url); } - }, []); + }; - const getCurrentHolds = useCallback(() => models, [models]); + const getCurrentHolds = () => models; useImperativeHandle( ref, @@ -81,12 +88,11 @@ const SidebarHoldsSection = forwardRef< addHold: addHoldToLocal, getCurrentHolds, }), - [addHoldToLocal, getCurrentHolds] + [models] ); const handleDelete = async (hold: HoldModel) => { - setDeletedHoldTypeIds((prev) => new Set([...prev, String(hold.hold_type?.id)])); - setLocallyAddedHolds((prev) => prev.filter((m) => m.hold_type?.id !== hold.hold_type?.id)); + setModels(models.filter((m) => m.hold_type.id !== hold.hold_type.id)); posthog.capture({ distinctId: 'demo', event: 'hold removed from collection', @@ -119,7 +125,7 @@ const SidebarHoldsSection = forwardRef< e.preventDefault(); startDrag({ type: "hold", - url: (model.hold_type.glb_url as string | undefined) ?? '', + url: model.hold_type.glb_url, customRotation: 0, }); const end = () => { @@ -137,13 +143,13 @@ const SidebarHoldsSection = forwardRef< onOpenInfo, }: { hold: HoldModel; - stockData: { holds?: Array<{ id: string | number; available_count?: number; used_count?: number; color?: string }> } | undefined; + stockData: any; onOpenInfo: (hold: HoldModel) => void; }) => { if (!hold || !hold.hold_type) return null; const currentHoldData = stockData?.holds?.find( - (stock) => stock.id === hold.id + (stock: any) => stock.id === hold.id ); const available = currentHoldData?.available_count || 0; const used = currentHoldData?.used_count || 0; diff --git a/frontend/src/features/editor/components/useDragPreview.ts b/frontend/src/features/editor/components/useDragPreview.ts index 27d67d6..d925194 100644 --- a/frontend/src/features/editor/components/useDragPreview.ts +++ b/frontend/src/features/editor/components/useDragPreview.ts @@ -9,7 +9,7 @@ function calculateHoldRotation( upVector: "X" | "Y" | "Z" = "Y" ): [number, number, number] { try { - const faceNormal = normal.clone(); + let faceNormal = normal.clone(); faceNormal.transformDirection(surface.matrixWorld); faceNormal.normalize(); if (Math.abs(faceNormal.y) > 0.99) { @@ -53,7 +53,7 @@ function calculateHoldRotation( const finalRotation = new THREE.Euler(); finalRotation.setFromRotationMatrix(finalRotationMatrix); return [finalRotation.x, finalRotation.y, finalRotation.z]; - } catch { + } catch (error) { return [-Math.PI / 2, 0, 0]; } } @@ -160,10 +160,10 @@ export function useDragPreview({ const q = computeAlignedQuaternion(normal, hit.object); setAlignedQuat([q.x, q.y, q.z, q.w]); // Compose with customRotation if present - const finalQ = q.clone(); + let finalQ = q.clone(); let customAngle = 0; - if (model && 'customRotation' in model && typeof model.customRotation === "number") { - customAngle = model.customRotation; + if (model && typeof (model as any).customRotation === "number") { + customAngle = (model as any).customRotation; } if (customAngle) { const customQ = new THREE.Quaternion(); @@ -174,7 +174,7 @@ export function useDragPreview({ // Determine drop target type let foundHoldId: string | undefined = undefined; let foundWall = false; - let o: THREE.Object3D | null = hit.object; + let o: any = hit.object; while (o) { if (o.userData && o.userData.placedObjectId) { foundHoldId = o.userData.placedObjectId; diff --git a/frontend/src/features/editor/store.ts b/frontend/src/features/editor/store.ts index 6a72795..2e843fc 100644 --- a/frontend/src/features/editor/store.ts +++ b/frontend/src/features/editor/store.ts @@ -1,46 +1,5 @@ import { create } from "zustand"; -export interface HoldType { - id?: string | number; - cdn_ref?: string; - manufacturer_ref?: string; - manufacturer?: string | { name?: string; [key: string]: unknown }; - model?: string; - glb_url?: string; - sprite_sheet_url?: string; - hold_usage_type?: string; - [key: string]: unknown; -} - -export interface HoldModel { - name: string; - file: string; - hold_type: HoldType; - hold_instance_id: string; - id?: string; -} - -export type SessionHoldInstance = { - id: string; - hold_instance_id?: string; - hold_type?: HoldType; - [key: string]: unknown; -}; - -export interface SessionData { - id?: string | number; - session_name?: string; - layout?: string | Record; - related_wall?: { - id?: string | number; - glb_url?: string; - [key: string]: unknown; - }; - related_holds_collection?: unknown; - holds_collection_instances?: SessionHoldInstance[]; - [key: string]: unknown; -} - export type DragModel = { type: "wall" | "hold"; url: string; diff --git a/frontend/src/features/editor/stubs/Hold360.tsx b/frontend/src/features/editor/stubs/Hold360.tsx index cc21702..7f99a61 100644 --- a/frontend/src/features/editor/stubs/Hold360.tsx +++ b/frontend/src/features/editor/stubs/Hold360.tsx @@ -1,6 +1,5 @@ import { createContext, useContext, useRef, useState, useEffect } from "react"; -// eslint-disable-next-line react-refresh/only-export-components export const HoldScrollContext = createContext | null>(null); const COLS = 6; @@ -8,8 +7,6 @@ const ROWS = 6; const TOTAL_FRAMES = COLS * ROWS; const FRAME_INTERVAL_MS = 30; -type HoldWithType = { hold_type?: { sprite_sheet_url?: string } }; - export default function Hold360({ cdn_ref: _cdn_ref, hold, @@ -17,11 +14,11 @@ export default function Hold360({ setCurrentDownloadUrl: _setCurrentDownloadUrl, }: { cdn_ref?: string; - hold?: HoldWithType; + hold?: unknown; className?: string; setCurrentDownloadUrl?: (url: string) => void; }) { - const sprite_sheet_url = hold?.hold_type?.sprite_sheet_url; + const sprite_sheet_url: string | undefined = (hold as any)?.hold_type?.sprite_sheet_url; const [isVisible, setIsVisible] = useState(false); const [isLoaded, setIsLoaded] = useState(false); const [frame, setFrame] = useState(0); diff --git a/frontend/src/features/editor/utils/HandleAddHold.tsx b/frontend/src/features/editor/utils/HandleAddHold.tsx index 6817242..11660d5 100644 --- a/frontend/src/features/editor/utils/HandleAddHold.tsx +++ b/frontend/src/features/editor/utils/HandleAddHold.tsx @@ -1,22 +1,30 @@ -import type { HoldType, HoldModel } from "../store"; +interface HoldType { + id: string | number; + cdn_ref: string; + manufacturer_ref?: string; + manufacturer?: string; + model?: string; + glb_url?: string; // provided by the serializer (HF CDN URL) + [key: string]: unknown; +} interface SelectedHold { - id: string | number; + id: string; hold_type: HoldType; } export default async function HandleAddHold( selectedHold: SelectedHold, _session_data: unknown, - onHoldAdded: ((hold: HoldModel) => void) | undefined + onHoldAdded: ((hold: any) => void) | undefined ) { try { - const newHoldInstance: HoldModel = { - hold_instance_id: String(selectedHold.id), - id: String(selectedHold.id), - name: (selectedHold.hold_type.manufacturer_ref as string | undefined) - ?? `${(selectedHold.hold_type.manufacturer as string | undefined) ?? ''} ${selectedHold.hold_type.model ?? ''}`.trim(), - file: (selectedHold.hold_type.cdn_ref as string) ?? '', + const newHoldInstance = { + hold_instance_id: selectedHold.id, + id: selectedHold.id, + name: selectedHold.hold_type.manufacturer_ref + ?? `${selectedHold.hold_type.manufacturer ?? ''} ${selectedHold.hold_type.model ?? ''}`.trim(), + file: selectedHold.hold_type.cdn_ref, hold_type: { ...selectedHold.hold_type, // glb_url is already set by the serializer — no need to reconstruct it diff --git a/frontend/src/features/editor/utils/HandleLoadSession.tsx b/frontend/src/features/editor/utils/HandleLoadSession.tsx index c47035f..968c46a 100644 --- a/frontend/src/features/editor/utils/HandleLoadSession.tsx +++ b/frontend/src/features/editor/utils/HandleLoadSession.tsx @@ -1,9 +1,9 @@ import { useCallback } from "react"; import { useEditorAuth } from "../mocks/useEditorAuth"; import { usePlacementStore } from "../store"; -import type { SessionData } from "../store"; -export const useHandleLoadSession = (session_data: SessionData) => { + +export const useHandleLoadSession = (session_data: any) => { const { authenticatedFetch } = useEditorAuth(); const setObjects = usePlacementStore((s) => s.setObjects); @@ -41,10 +41,9 @@ export const useHandleLoadSession = (session_data: SessionData) => { const currentObjects = usePlacementStore.getState().objects; const existingWall = currentObjects.find((obj) => obj.type === "wall"); - type LayoutObject = { type?: string; url?: string; wall_id?: string; [key: string]: unknown }; - const sessionObjects: LayoutObject[] = data.objects || []; - const sessionWall = sessionObjects.find((obj) => obj.type === "wall"); - const sessionHolds = sessionObjects.filter((obj) => obj.type !== "wall"); + const sessionObjects: any[] = data.objects || []; + const sessionWall = sessionObjects.find((obj: any) => obj.type === "wall"); + const sessionHolds = sessionObjects.filter((obj: any) => obj.type !== "wall"); let wallToUse = null; if (sessionWall && sessionWall.url) { diff --git a/frontend/src/features/editor/utils/useHoldAvailabilityValidation.tsx b/frontend/src/features/editor/utils/useHoldAvailabilityValidation.tsx index c426844..d1b837d 100644 --- a/frontend/src/features/editor/utils/useHoldAvailabilityValidation.tsx +++ b/frontend/src/features/editor/utils/useHoldAvailabilityValidation.tsx @@ -1,24 +1,11 @@ import { useEditorAuth } from "../mocks/useEditorAuth"; -type SessionDataWithLayout = { - id?: string | number; - layout?: string | { objects?: Array<{ type?: string; url?: string; [key: string]: unknown }> }; - [key: string]: unknown; -}; - -type StockHold = { - id?: string | number; - hold_type?: { id?: string | number; model?: string; manufacturer_ref?: string; cdn_ref?: string; manufacturer?: string | { name?: string } }; - available_count?: number; - last_used_wall?: { wall_name?: string; session_name?: string; wall_id?: string | number; session_id?: string | number }; -}; - export function useHoldAvailabilityValidation() { const { authenticatedFetch, user } = useEditorAuth(); const API_URL = import.meta.env.VITE_API_BASE; const gym_id = user?.related_gym_id; - const validateHoldAvailability = async (sessionData: SessionDataWithLayout) => { + const validateHoldAvailability = async (sessionData: any) => { try { const stockResponse = await authenticatedFetch( `${API_URL}/gym/stock-explore/${gym_id}/?page_size=1000` @@ -29,7 +16,7 @@ export function useHoldAvailabilityValidation() { } const stockData = await stockResponse.json(); - const stockHolds: StockHold[] = stockData.holds || []; + const stockHolds = stockData.holds || []; let layout = sessionData.layout; if (typeof layout === "string") { @@ -46,7 +33,7 @@ export function useHoldAvailabilityValidation() { } const requiredHolds: Record = {}; - layout.objects.forEach((obj) => { + layout.objects.forEach((obj: any) => { if (obj.type === "hold") { const holdTypeId = extractHoldTypeIdFromUrl(obj.url); if (holdTypeId) { @@ -61,7 +48,7 @@ export function useHoldAvailabilityValidation() { const unavailableHolds = []; for (const [holdTypeId, requiredCount] of Object.entries(requiredHolds)) { - const stockHold = stockHolds.find((h) => h.hold_type?.id == holdTypeId); + const stockHold = stockHolds.find((h: any) => h.hold_type?.id == holdTypeId); if (!stockHold) { unavailableHolds.push({ hold_type_id: holdTypeId, @@ -81,11 +68,10 @@ export function useHoldAvailabilityValidation() { stockHold.hold_type?.manufacturer_ref || stockHold.hold_type?.cdn_ref || `Hold Type ${holdTypeId}`, - manufacturer: (() => { - const mfr = stockHold.hold_type?.manufacturer; - if (typeof mfr === 'object' && mfr !== null) return mfr.name ?? "Unknown"; - return (mfr as string | undefined) ?? "Unknown"; - })(), + manufacturer: + stockHold.hold_type?.manufacturer?.name || + stockHold.hold_type?.manufacturer || + "Unknown", required_count: requiredCount, available_count: availableCount, current_location: diff --git a/frontend/src/shared/auth/AuthProvider.tsx b/frontend/src/shared/auth/AuthProvider.tsx index 3f6969a..c31240e 100644 --- a/frontend/src/shared/auth/AuthProvider.tsx +++ b/frontend/src/shared/auth/AuthProvider.tsx @@ -48,7 +48,6 @@ export function AuthProvider({ children }: { children: ReactNode }) { ); } -// eslint-disable-next-line react-refresh/only-export-components export function useAuth(): AuthContextValue { const ctx = useContext(AuthContext); if (!ctx) {