From 0a13aedbefc14109e0570251f14dc8934b3b843a Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 22 Apr 2026 08:31:25 -0500 Subject: [PATCH] Use Bootstrap Icons in clip menu --- frontend/package-lock.json | 17 ++ frontend/package.json | 1 + frontend/src/main.tsx | 1 + frontend/src/pages/LibraryPage.tsx | 376 +++++++++++++++++++---------- frontend/src/styles.css | 137 +++++++++-- 5 files changed, 387 insertions(+), 145 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0818d3a..41e6837 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@tanstack/react-query": "^5.59.0", "bootstrap": "^5.3.8", + "bootstrap-icons": "^1.13.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.28.0", @@ -2857,6 +2858,22 @@ "@popperjs/core": "^2.11.8" } }, + "node_modules/bootstrap-icons": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz", + "integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index c353ae7..66e1596 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "dependencies": { "@tanstack/react-query": "^5.59.0", "bootstrap": "^5.3.8", + "bootstrap-icons": "^1.13.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.28.0", diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 7b641b4..0f132e2 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -6,6 +6,7 @@ import { BrowserRouter } from "react-router-dom"; import App from "./App"; import { queryClient } from "./lib/queryClient"; import "bootstrap/dist/css/bootstrap.css"; +import "bootstrap-icons/font/bootstrap-icons.css"; import "./styles.css"; ReactDOM.createRoot(document.getElementById("root")!).render( diff --git a/frontend/src/pages/LibraryPage.tsx b/frontend/src/pages/LibraryPage.tsx index 900d69e..524e41d 100644 --- a/frontend/src/pages/LibraryPage.tsx +++ b/frontend/src/pages/LibraryPage.tsx @@ -19,6 +19,14 @@ const TRIM_STEP_MS = 100; const TRIM_ZOOM_WINDOW_MS = 3_000; const TRIM_ZOOM_SLIDER_MAX = 100; +function clipsQueryKey(teamId: string | null, playerId: string | null, includeHidden: boolean) { + return ["clips", teamId, playerId, includeHidden ? "all" : "visible"] as const; +} + +function clipsQueryPrefix(teamId: string | null, playerId: string | null) { + return ["clips", teamId, playerId] as const; +} + type WalkupClipSourceMode = "upload" | "url" | "existing"; type WalkupClipModalState = @@ -28,13 +36,15 @@ type WalkupClipModalState = type BootstrapIconName = | "play" | "stop" - | "three-dots-vertical" + | "three-dots" | "pencil-square" | "plus-lg" | "x-lg" | "chevron-up" | "chevron-down" - | "pin-fill"; + | "pin-fill" + | "eye" + | "eye-slash"; type TrimFocusEdge = "start" | "end"; type SourceCreationProgress = { label: string; @@ -47,6 +57,7 @@ export function LibraryPage() { const teamId = walkup.selectedTeamId; const playerId = walkup.currentPlayerId; const [walkupClipModal, setWalkupClipModal] = useState(null); + const [clipPinModalClipId, setClipPinModalClipId] = useState(null); const [manageMediaOpen, setManageMediaOpen] = useState(false); const audioRef = useRef(null); const previewClipIdRef = useRef(null); @@ -60,8 +71,8 @@ export function LibraryPage() { enabled: Boolean(teamId && playerId), }); const clipsQuery = useQuery({ - queryKey: ["clips", teamId, playerId], - queryFn: () => api.listClips(teamId, playerId), + queryKey: clipsQueryKey(teamId, playerId, true), + queryFn: () => api.listClips(teamId, playerId, true), enabled: Boolean(teamId && playerId), }); const pinsQuery = useQuery({ @@ -82,6 +93,13 @@ export function LibraryPage() { const pinnedAssignmentsByClipAndGame = useMemo(() => { return new Map((pinsQuery.data ?? []).map((assignment) => [`${assignment.external_game_id}:${assignment.clip_id}`, assignment])); }, [pinsQuery.data]); + const pinCountByClipId = useMemo(() => { + const counts = new Map(); + for (const assignment of pinsQuery.data ?? []) { + counts.set(assignment.clip_id, (counts.get(assignment.clip_id) ?? 0) + 1); + } + return counts; + }, [pinsQuery.data]); useEffect(() => { return () => { @@ -93,7 +111,7 @@ export function LibraryPage() { mutationFn: (clipId: number) => api.deleteClip(clipId, playerId), onSuccess: async () => { stopPreview(); - await queryClient.invalidateQueries({ queryKey: ["clips", teamId, playerId] }); + await queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) }); }, }); @@ -105,7 +123,29 @@ export function LibraryPage() { clip_ids: clipIds, }), onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ["clips", teamId, playerId] }); + await queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) }); + }, + }); + + const toggleHiddenMutation = useMutation({ + mutationFn: async ({ clip, hidden }: { clip: AudioClip; hidden: boolean }) => + api.updateClip( + clip.id, + { + label: clip.label, + start_ms: clip.start_ms, + end_ms: clip.end_ms, + sort_order: clip.sort_order, + hidden, + }, + playerId, + ), + onSuccess: async () => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) }), + queryClient.invalidateQueries({ queryKey: ["assignments"] }), + queryClient.invalidateQueries({ queryKey: ["prep"] }), + ]); }, }); @@ -259,7 +299,7 @@ export function LibraryPage() { return (
-
+

My Clips

@@ -304,11 +344,12 @@ export function LibraryPage() { }} canMoveUp={index > 0} canMoveDown={index < orderedClips.length - 1} - games={walkup.games} - pinnedAssignmentsByClipAndGame={pinnedAssignmentsByClipAndGame} - onTogglePin={(gameId) => { - void togglePinMutation.mutateAsync({ clipId: clip.id, gameId }); + pinCount={pinCountByClipId.get(clip.id) ?? 0} + onOpenPinModal={() => setClipPinModalClipId(clip.id)} + onToggleHidden={() => { + void toggleHiddenMutation.mutateAsync({ clip, hidden: !clip.hidden }); }} + isHidden={clip.hidden} /> ))} {!clipsQuery.isLoading && !orderedClips.length ? ( @@ -345,6 +386,18 @@ export function LibraryPage() { isDeletingClip={deleteClipMutation.isPending} /> ) : null} + {clipPinModalClipId !== null ? ( + item.id === clipPinModalClipId) ?? null} + games={walkup.games} + pinnedAssignmentsByClipAndGame={pinnedAssignmentsByClipAndGame} + onClose={() => setClipPinModalClipId(null)} + onTogglePin={(gameId, clipId) => { + void togglePinMutation.mutateAsync({ clipId, gameId }); + }} + isTogglingPin={togglePinMutation.isPending} + /> + ) : null} {manageMediaOpen ? (