From 3bf395089900272a77f673eb8efa18af46485a1a Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 22 Apr 2026 16:10:40 -0500 Subject: [PATCH] Rename operator view to gameday --- PLAN.md | 4 +- README.md | 4 +- backend/app/models.py | 4 +- backend/app/routes/games.py | 10 +- backend/tests/fixtures/teamsnap/README.md | 2 +- docs/architecture.md | 2 +- frontend/src/App.tsx | 8 +- frontend/src/api/client.ts | 12 +-- frontend/src/pages/GamePage.tsx | 2 +- .../{OperatorPage.tsx => GamedayPage.tsx} | 62 ++++++------ frontend/src/styles.css | 96 +++++++++---------- 11 files changed, 103 insertions(+), 103 deletions(-) rename frontend/src/pages/{OperatorPage.tsx => GamedayPage.tsx} (91%) diff --git a/PLAN.md b/PLAN.md index 8403c85..884c0b3 100644 --- a/PLAN.md +++ b/PLAN.md @@ -10,11 +10,11 @@ ## Initial Deliverables - Thin TeamSnap auth/session backend. - Media upload and clip registration flow. -- Game assignment and operator session APIs. +- Game assignment and gameday session APIs. - Installable React PWA shell with offline-ready game prep scaffolding. - Docker-based local development stack. ## Known Constraints - TeamSnap entities should not be durably mirrored on the backend. -- Operator lineup changes are local-session state in v1. +- Gameday lineup changes are local-session state in v1. - Browser clip editing is first-class; backend finalizes playback assets. diff --git a/README.md b/README.md index 42c5925..b34c173 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,11 @@ Walkup is a collaborative baseball walk-up song app built as a React PWA with a - TeamSnap OAuth start/callback/refresh - Session cookie management - Media upload and normalized clip registration -- Game assignments and operator session APIs +- Game assignments and gameday session APIs ## Frontend Responsibilities - TeamSnap SDK bootstrap with server-issued access tokens - Team/game browsing from TeamSnap - Song upload and clip creation -- Game assignments and operator console +- Game assignments and gameday console - PWA install/offline shell diff --git a/backend/app/models.py b/backend/app/models.py index b634d2e..d1c3d97 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -89,12 +89,12 @@ class PlaybackSession(Base): id: Mapped[int] = mapped_column(primary_key=True) external_team_id: Mapped[str] = mapped_column(String(128), index=True) external_game_id: Mapped[str] = mapped_column(String(128), index=True) - operator_session_id: Mapped[int | None] = mapped_column(ForeignKey("user_sessions.id")) + gameday_session_id: Mapped[int | None] = mapped_column(ForeignKey("user_sessions.id")) current_assignment_id: Mapped[int | None] = mapped_column(ForeignKey("game_assignments.id")) state: Mapped[str] = mapped_column(String(32), default="idle") last_triggered_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, onupdate=utcnow) - operator_session: Mapped[UserSession | None] = relationship() + gameday_session: Mapped[UserSession | None] = relationship() current_assignment: Mapped[GameAssignment | None] = relationship() diff --git a/backend/app/routes/games.py b/backend/app/routes/games.py index f4ad6ec..71cdb6a 100644 --- a/backend/app/routes/games.py +++ b/backend/app/routes/games.py @@ -159,8 +159,8 @@ def prepare_game( ) -@router.post("/{external_game_id}/operator/session", response_model=PlaybackSessionResponse) -def create_playback_session( +@router.post("/{external_game_id}/gameday/session", response_model=PlaybackSessionResponse) +def create_gameday_session( external_game_id: str, payload: PlaybackSessionCreate, session: UserSession = Depends(require_session), @@ -169,7 +169,7 @@ def create_playback_session( playback = PlaybackSession( external_team_id=payload.external_team_id, external_game_id=external_game_id, - operator_session_id=session.id, + gameday_session_id=session.id, state="idle", ) db.add(playback) @@ -178,8 +178,8 @@ def create_playback_session( return PlaybackSessionResponse.model_validate(playback, from_attributes=True) -@router.post("/{external_game_id}/operator/session/{playback_session_id}/trigger", response_model=PlaybackSessionResponse) -def trigger_playback( +@router.post("/{external_game_id}/gameday/session/{playback_session_id}/trigger", response_model=PlaybackSessionResponse) +def trigger_gameday( external_game_id: str, playback_session_id: int, payload: PlaybackAction, diff --git a/backend/tests/fixtures/teamsnap/README.md b/backend/tests/fixtures/teamsnap/README.md index ddfecfd..5b182c8 100644 --- a/backend/tests/fixtures/teamsnap/README.md +++ b/backend/tests/fixtures/teamsnap/README.md @@ -20,6 +20,6 @@ They are intentionally small but cover the collections this app reads: - `me` for auth/session identity - `teams` for team selection - `members` for player lookup -- `events` for the operator/game flow +- `events` for the gameday/game flow - `availabilities`, `assignments`, `eventLineups`, and `eventLineupEntries` for lineup and game preparation screens diff --git a/docs/architecture.md b/docs/architecture.md index aa3ac56..24d25ae 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -14,7 +14,7 @@ Walkup is a baseball walk-up song app with a React PWA frontend and a FastAPI ba - `frontend/` contains the React application. - The app uses React Router for navigation and TanStack Query for server state. - TeamSnap data is loaded through the official JavaScript SDK from the browser after the backend provides an access token. -- The UI includes player, operator, and library views for clip management and gameday playback. +- The UI includes player, gameday, and library views for clip management and gameday playback. - The app is shipped as a PWA with install and offline-prep behavior. ## Backend diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9f05e5a..efbe49e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,8 +4,8 @@ import { NavLink, Navigate, Route, Routes, useLocation } from "react-router-dom" import { WalkupProvider, useWalkupContext } from "./hooks/useWalkupContext"; import { useSession } from "./hooks/useSession"; import { DashboardPage } from "./pages/DashboardPage"; +import { GamedayPage } from "./pages/GamedayPage"; import { LibraryPage } from "./pages/LibraryPage"; -import { OperatorPage } from "./pages/OperatorPage"; import { ProfilePage } from "./pages/ProfilePage"; import { AdminPage } from "./pages/AdminPage"; import { SignInPage } from "./pages/SignInPage"; @@ -17,7 +17,7 @@ function getRouteDestinationLabel(pathname: string) { return "your dashboard"; case "/library": return "walkup clips"; - case "/operator": + case "/gameday": return "gameday"; default: return "this page"; @@ -257,7 +257,7 @@ function ShellLayout() { `nav-link${isActive ? " active" : ""}`}> Walkup Clips - `nav-link${isActive ? " active" : ""}`}> + `nav-link${isActive ? " active" : ""}`}> Gameday `nav-link${isActive ? " active" : ""}`}> @@ -276,7 +276,7 @@ function ShellLayout() { } /> } /> } /> - } /> + } /> {showTeamSelectionModal ? : null} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 96991a9..db644fb 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -196,18 +196,18 @@ export const api = { } }), prepareGame: (gameId: string) => request(`/games/${encodeURIComponent(gameId)}/prep`), - createPlaybackSession: (gameId: string, teamId: string) => - request(`/games/${encodeURIComponent(gameId)}/operator/session`, { + createGamedaySession: (gameId: string, teamId: string) => + request(`/games/${encodeURIComponent(gameId)}/gameday/session`, { method: "POST", body: JSON.stringify({ external_team_id: teamId }), }), - triggerPlaybackAssignment: (gameId: string, playbackSessionId: number, assignmentId: number) => - request(`/games/${encodeURIComponent(gameId)}/operator/session/${playbackSessionId}/trigger`, { + triggerGamedayAssignment: (gameId: string, playbackSessionId: number, assignmentId: number) => + request(`/games/${encodeURIComponent(gameId)}/gameday/session/${playbackSessionId}/trigger`, { method: "POST", body: JSON.stringify({ assignment_id: assignmentId, state: "playing" }), }), - triggerPlaybackClip: (gameId: string, playbackSessionId: number, clipId: number, playerId: string) => - request(`/games/${encodeURIComponent(gameId)}/operator/session/${playbackSessionId}/trigger`, { + triggerGamedayClip: (gameId: string, playbackSessionId: number, clipId: number, playerId: string) => + request(`/games/${encodeURIComponent(gameId)}/gameday/session/${playbackSessionId}/trigger`, { method: "POST", body: JSON.stringify({ clip_id: clipId, external_player_id: playerId, state: "playing" }), }), diff --git a/frontend/src/pages/GamePage.tsx b/frontend/src/pages/GamePage.tsx index 2dd9e20..9b8385d 100644 --- a/frontend/src/pages/GamePage.tsx +++ b/frontend/src/pages/GamePage.tsx @@ -84,7 +84,7 @@ export function GamePage() { return; } savePreparedGame(selectedGameId, prepQuery.data); - setOfflineMessage(`Cached ${prepQuery.data.assignments.length} pinned clips for offline operator use.`); + setOfflineMessage(`Cached ${prepQuery.data.assignments.length} pinned clips for offline gameday use.`); } const selectedGame = walkup.games.find((game) => String(game.id) === selectedGameId) ?? null; diff --git a/frontend/src/pages/OperatorPage.tsx b/frontend/src/pages/GamedayPage.tsx similarity index 91% rename from frontend/src/pages/OperatorPage.tsx rename to frontend/src/pages/GamedayPage.tsx index 63d8071..7e1802d 100644 --- a/frontend/src/pages/OperatorPage.tsx +++ b/frontend/src/pages/GamedayPage.tsx @@ -56,7 +56,7 @@ function getAvailabilityDotLabel(statusCode: number | null | undefined): string return "Availability unset"; } -export function OperatorPage() { +export function GamedayPage() { const walkup = useWalkupContext(); const [searchParams, setSearchParams] = useSearchParams(); const [selectedGameId, setSelectedGameId] = useState(searchParams.get("gameId") ?? ""); @@ -217,7 +217,7 @@ export function OperatorPage() { ); const createSession = useMutation({ - mutationFn: () => api.createPlaybackSession(selectedGameId, teamId), + mutationFn: () => api.createGamedaySession(selectedGameId, teamId), onSuccess: (session) => setPlaybackSessionId(session.id), }); @@ -226,7 +226,7 @@ export function OperatorPage() { if (!playbackSessionId) { throw new Error("Start a gameday session first"); } - return api.triggerPlaybackClip(selectedGameId, playbackSessionId, clip.id, selectedPlayerId); + return api.triggerGamedayClip(selectedGameId, playbackSessionId, clip.id, selectedPlayerId); }, }); @@ -415,15 +415,15 @@ export function OperatorPage() { } return ( -
+
{isPlaybackPlaying && nowPlaying ? ( -
-
- Now Playing +
+
+ Now Playing {nowPlaying.title} {nowPlaying.subtitle}
-
+
@@ -462,9 +462,9 @@ export function OperatorPage() {
-
+

Players

-
+
{playerFilterMenuOpen ? ( -
+
-
+
{visibleMembers.map((member) => { const memberId = String(member.id); const jerseyNumber = formatMemberJerseyNumber(member); @@ -538,10 +538,10 @@ export function OperatorPage() { ].filter(Boolean); return ( -
+
{isExpanded ? (
-
-
+
+
-
-
+
+
Debug: Show raw lineup data -
+                          
                             {JSON.stringify(
                               {
                                 selectedPlayerId,
@@ -680,9 +680,9 @@ function LibraryClips({
         const isPlaying = playingClipKey === key;
         const isPinned = pinnedAssignmentsByClipId.has(String(clip.id));
         return (
-          
-
- +
+
+ {clip.label} {isPinned ? Pinned : null} @@ -690,11 +690,11 @@ function LibraryClips({
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index b22f24f..241e470 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -47,7 +47,7 @@ select { isolation: isolate; } -.operator-page { +.gameday-page { padding-bottom: 112px; } @@ -316,7 +316,7 @@ select { flex: 0 0 auto; } -.operator-clip-title { +.gameday-clip-title { display: inline-flex; align-items: center; gap: 0.45rem; @@ -355,14 +355,14 @@ select { font-size: 1rem; } -.operator-panel-header { +.gameday-panel-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.75rem; } -.operator-filter-menu-wrap { +.gameday-filter-menu-wrap { position: relative; display: flex; align-items: center; @@ -376,7 +376,7 @@ select { fill: currentColor; } -.operator-filter-menu { +.gameday-filter-menu { position: absolute; top: calc(100% + 0.4rem); right: 0; @@ -391,7 +391,7 @@ select { gap: 0.2rem; } -.operator-filter-menu-item { +.gameday-filter-menu-item { width: 100%; border: 0; background: transparent; @@ -402,20 +402,20 @@ select { font-size: 0.92rem; } -.operator-filter-menu-item.is-active { +.gameday-filter-menu-item.is-active { background: rgba(217, 79, 4, 0.1); color: var(--accent); font-weight: 600; } @media (hover: hover) and (pointer: fine) { - .operator-filter-menu-item:hover { + .gameday-filter-menu-item:hover { background: rgba(19, 34, 56, 0.06); } } -.operator-filter-menu-item:focus-visible, -.operator-filter-button:focus-visible { +.gameday-filter-menu-item:focus-visible, +.gameday-filter-button:focus-visible { outline: 2px solid rgba(217, 79, 4, 0.45); outline-offset: 2px; } @@ -895,7 +895,7 @@ select { box-shadow: 0 0 0 2px rgba(217, 79, 4, 0.12); } -.operator-toolbar { +.gameday-toolbar { position: fixed; right: 1.75rem; bottom: 1.15rem; @@ -913,34 +913,34 @@ select { z-index: 20; } -.operator-toolbar-copy { +.gameday-toolbar-copy { display: grid; gap: 0.25rem; min-width: 0; } -.operator-toolbar-label { +.gameday-toolbar-label { color: rgba(19, 34, 56, 0.58); font-size: 0.74rem; letter-spacing: 0.12em; text-transform: uppercase; } -.operator-toolbar-copy strong, -.operator-toolbar-copy .muted { +.gameday-toolbar-copy strong, +.gameday-toolbar-copy .muted { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.operator-toolbar-actions { +.gameday-toolbar-actions { display: flex; align-items: center; gap: 0.65rem; flex: 0 0 auto; } -.operator-player-list { +.gameday-player-list { gap: 0; padding: 0; border: 1px solid var(--panel-border); @@ -950,7 +950,7 @@ select { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5); } -.operator-player-card { +.gameday-player-card { display: grid; gap: 0; padding: 0; @@ -959,35 +959,35 @@ select { background: transparent; } -.operator-player-card:first-child { +.gameday-player-card:first-child { border-top: 0; } -.operator-player-card.is-selected { +.gameday-player-card.is-selected { background: rgba(217, 79, 4, 0.03); } -.operator-player-summary { +.gameday-player-summary { display: grid; gap: 0.3rem; flex: 1 1 auto; text-align: left; } -.operator-player-heading { +.gameday-player-heading { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; } -.operator-player-heading strong { +.gameday-player-heading strong { display: inline-flex; align-items: center; gap: 0.5rem; } -.operator-availability-dot { +.gameday-availability-dot { width: 0.7rem; height: 0.7rem; flex: 0 0 auto; @@ -996,23 +996,23 @@ select { box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.88); } -.operator-availability-dot.is-yes { +.gameday-availability-dot.is-yes { background: #2f9e44; } -.operator-availability-dot.is-no { +.gameday-availability-dot.is-no { background: #e03131; } -.operator-availability-dot.is-maybe { +.gameday-availability-dot.is-maybe { background: #1c7ed6; } -.operator-availability-dot.is-blank { +.gameday-availability-dot.is-blank { background: #adb5bd; } -.operator-player-toggle { +.gameday-player-toggle { --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23132238' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23d94f04' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); --bs-accordion-btn-icon-width: 1.25rem; @@ -1020,7 +1020,7 @@ select { --bs-accordion-btn-icon-transition: transform 0.2s ease-in-out; } -.operator-player-toggle::after { +.gameday-player-toggle::after { flex-shrink: 0; width: var(--bs-accordion-btn-icon-width); height: var(--bs-accordion-btn-icon-width); @@ -1032,18 +1032,18 @@ select { transition: var(--bs-accordion-btn-icon-transition); } -.operator-player-toggle.active::after { +.gameday-player-toggle.active::after { background-image: var(--bs-accordion-btn-active-icon); transform: var(--bs-accordion-btn-icon-transform); } @media (prefers-reduced-motion: reduce) { - .operator-player-toggle::after { + .gameday-player-toggle::after { transition: none; } } -.operator-expansion { +.gameday-expansion { display: grid; gap: 1rem; padding: 0; @@ -1051,17 +1051,17 @@ select { background: rgba(255, 255, 255, 0.94); } -.operator-section { +.gameday-section { display: grid; gap: 0.65rem; padding: 1rem 1rem 0; } -.operator-section:last-child { +.gameday-section:last-child { padding-bottom: 1rem; } -.operator-section-title { +.gameday-section-title { display: flex; justify-content: space-between; align-items: baseline; @@ -1069,12 +1069,12 @@ select { flex-wrap: wrap; } -.operator-clip-list { +.gameday-clip-list { display: grid; gap: 0.5rem; } -.operator-clip-row { +.gameday-clip-row { display: flex; align-items: center; justify-content: flex-start; @@ -1085,29 +1085,29 @@ select { background: rgba(255, 255, 255, 0.75); } -.operator-clip-copy { +.gameday-clip-copy { display: grid; gap: 0.25rem; flex: 1 1 auto; min-width: 0; } -.operator-clip-play-button { +.gameday-clip-play-button { margin-left: auto; } -.operator-clip-button-indicator { +.gameday-clip-button-indicator { width: 0.55rem; height: 0.55rem; border-radius: 999px; background: rgba(19, 34, 56, 0.32); } -.operator-clip-button-indicator.is-playing { +.gameday-clip-button-indicator.is-playing { background: var(--accent); } -.operator-debug { +.gameday-debug { margin: 0; padding: 0.75rem 0.85rem; border-radius: 0.75rem; @@ -1120,28 +1120,28 @@ select { overflow-x: auto; } -.operator-debug-details { +.gameday-debug-details { padding: 0.15rem 0 0; color: var(--muted); } -.operator-debug-details > summary { +.gameday-debug-details > summary { cursor: pointer; list-style: none; color: var(--muted); font-size: 0.88rem; } -.operator-debug-details > summary::-webkit-details-marker { +.gameday-debug-details > summary::-webkit-details-marker { display: none; } -.operator-debug-details[open] > summary { +.gameday-debug-details[open] > summary { margin-bottom: 0.65rem; } @media (max-width: 900px) { - .operator-toolbar { + .gameday-toolbar { left: 1rem; right: 1rem; }