diff --git a/PLAN.md b/PLAN.md index c60f83e..8ee0ecd 100644 --- a/PLAN.md +++ b/PLAN.md @@ -14,6 +14,11 @@ - Installable React PWA shell with offline-ready game prep scaffolding. - Docker-based local development stack. +## Completed UI Cleanup +- Home page now acts as a lightweight landing page with direct links to Library and Gameday. +- Removed the old game-list-heavy dashboard content that was not useful as a landing surface. +- Game titles in the UI now include a day parenthetical such as `(sun 5/3)` wherever the shared formatter is used. + ## Storage Status - Backend media persists in the `backend-media` named Docker volume. diff --git a/docs/architecture.md b/docs/architecture.md index 54b6a43..8271f08 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -15,6 +15,7 @@ Walkup is a baseball walk-up song app with a React PWA frontend and a FastAPI ba - 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, gameday, and library views for clip management and gameday playback. +- The home page is a lightweight landing page that orients users and links to the Library and Gameday views. - The app is shipped as a PWA with install and offline-prep behavior. ## Backend diff --git a/frontend/src/lib/teamsnapHelpers.ts b/frontend/src/lib/teamsnapHelpers.ts index 1cedc79..16427c5 100644 --- a/frontend/src/lib/teamsnapHelpers.ts +++ b/frontend/src/lib/teamsnapHelpers.ts @@ -75,14 +75,15 @@ export function findCurrentPlayer(externalUserId: string | number | null | undef export function formatGameTitle(game: TeamSnapEvent): string { const name = asDisplayText(game.name); + const dayLabel = formatGameDayLabel(game); if (name) { - return name; + return `${name}${dayLabel}`; } const opponentName = asDisplayText(game.opponentName); if (opponentName) { - return `vs ${opponentName}`; + return `vs ${opponentName}${dayLabel}`; } - return `Game ${game.id}`; + return `Game ${game.id}${dayLabel}`; } export function formatGameDate(game: TeamSnapEvent): string { @@ -98,6 +99,20 @@ export function formatGameDate(game: TeamSnapEvent): string { }); } +function formatGameDayLabel(game: TeamSnapEvent): string { + const date = toDate(game.startDate); + if (!date) { + return ""; + } + + const weekday = date + .toLocaleDateString("en-US", { weekday: "short" }) + .replace(/\./g, "") + .replace(/^./, (character) => character.toUpperCase()); + const monthDay = date.toLocaleDateString("en-US", { month: "numeric", day: "numeric" }); + return ` (${weekday} ${monthDay})`; +} + export function sortGames(events: TeamSnapEvent[]): TeamSnapEvent[] { return [...events] .filter((event) => event.isGame) diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index c311f09..c7cdcb0 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -1,20 +1,45 @@ -import { useNavigate } from "react-router-dom"; +import { Link } from "react-router-dom"; import { useWalkupContext } from "../hooks/useWalkupContext"; -import { formatGameDate, formatGameTitle, formatMemberName } from "../lib/teamsnapHelpers"; export function DashboardPage() { - const navigate = useNavigate(); const walkup = useWalkupContext(); if (!walkup.isTeamSnap) { return ( -
-
-
-

Player flow

-

Sign in with TeamSnap to resolve your player and team context.

-

The player dashboard depends on your TeamSnap user, roster membership, and upcoming games.

+
+
+
+
+
+

Library

+

Manage walkup clips

+

+ Upload audio, trim clips, reorder them, and pin them to players before game day. +

+
+ + Open Library + +
+
+
+
+
+
+
+

Gameday

+

Run the game-day view

+

+ Review lineups, check availability, and play the right walkup clips during the game. +

+
+ + Open Gameday + +
+
+
@@ -23,61 +48,36 @@ export function DashboardPage() { return (
-
-
-

Player dashboard

-

{walkup.nextGame ? formatGameTitle(walkup.nextGame) : "No upcoming game found yet."}

-

- {walkup.currentPlayer - ? `${formatMemberName(walkup.currentPlayer)} is ready for the selected team.` - : "Your TeamSnap user is connected, but no matching player record was found on the selected team."} -

-
-
-
+
-

Next game

- {walkup.nextGame ? ( - <> - {formatGameTitle(walkup.nextGame)} -
{formatGameDate(walkup.nextGame)}
- {walkup.nextGame.locationName ?
{walkup.nextGame.locationName}
: null} -
- +

Library

+

Manage walkup clips

+

+ Upload audio, trim clips, reorder them, and pin them to players before game day. +

+
+ + Open Library +
- - ) : ( -
No upcoming games were returned for this team.
- )}
-
+
-

Upcoming games

-
- {walkup.eventsQuery.isLoading ?
Loading games...
: null} - {walkup.games.slice(0, 8).map((game) => ( -
-
- {formatGameTitle(game)} -
{formatGameDate(game)}
-
- {String(game.id) === String(walkup.nextGame?.id) ? "Next" : "Browse"} +

Gameday

+

Run the game-day view

+

+ Review lineups, check availability, and play the right walkup clips during the game. +

+
+ + Open Gameday +
- ))} - {!walkup.eventsQuery.isLoading && !walkup.games.length ? ( -
No games were returned for the selected team.
- ) : null} -
diff --git a/frontend/src/pages/LibraryPage.tsx b/frontend/src/pages/LibraryPage.tsx index c878377..5906875 100644 --- a/frontend/src/pages/LibraryPage.tsx +++ b/frontend/src/pages/LibraryPage.tsx @@ -15,6 +15,8 @@ import { formatGameTitle, formatMemberName } from "../lib/teamsnapHelpers"; const MEDIA_ACCEPT = ".mp3,.m4a,.aac,.wav,.ogg,.oga,.flac,.mp4,.m4v,.mov,audio/*,video/*,application/octet-stream"; const DEFAULT_CLIP_LENGTH_MS = 30_000; +const END_SHORTCUT_LENGTH_MS = 90_000; +const SAVE_FADE_OUT_MS = 1000; const TRIM_NUDGE_MS = 100; const TRIM_STEP_MS = 100; const TRIM_ZOOM_WINDOW_MS = 3_000; @@ -63,6 +65,7 @@ export function LibraryPage() { const { activeKey: previewKey, currentTimeMs: previewTimeMs, + fadeOutClip, playClip: playClipPreview, stopClip: stopPreview, } = useClipPlayback(); @@ -314,6 +317,7 @@ export function LibraryPage() { teamId={teamId} playerId={playerId} previewTimeMs={previewTimeMs} + fadeOutPreview={fadeOutClip} playPreview={playPreview} onClose={closeCreateWalkupClip} stopPreview={stopPreview} @@ -373,6 +377,7 @@ function WalkupClipModal({ teamId, playerId, previewTimeMs, + fadeOutPreview, playPreview, onClose, stopPreview, @@ -384,6 +389,7 @@ function WalkupClipModal({ teamId: string; playerId: string; previewTimeMs: number | null; + fadeOutPreview: (durationMs?: number) => void; playPreview: (clip: AudioClip, startMs?: number, endMs?: number) => Promise; onClose: () => void; stopPreview: () => void; @@ -593,23 +599,19 @@ function WalkupClipModal({
-
-
1. Source
-
2. Trim and metadata
-
+ {step === "source" ? (
-
  • @@ -636,7 +638,7 @@ function WalkupClipModal({ disabled={createSourceMutation.isPending} onClick={() => setSourceMode("url")} > - Import URL + URL
  • @@ -650,7 +652,7 @@ function WalkupClipModal({ disabled={createSourceMutation.isPending} onClick={() => setSourceMode("existing")} > - Existing media + Existing
@@ -662,6 +664,7 @@ function WalkupClipModal({ className={`tab-pane fade${sourceMode === "upload" ? " show active" : ""}`} >
+
Upload a local audio file to create a new walkup clip.