219 lines
9.1 KiB
TypeScript
219 lines
9.1 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
import { useSearchParams } from "react-router-dom";
|
|
|
|
import { api } from "../api/client";
|
|
import { useWalkupContext } from "../hooks/useWalkupContext";
|
|
import { loadPreparedGame, savePreparedGame } from "../lib/offlinePrep";
|
|
import { queryClient } from "../lib/queryClient";
|
|
import { formatGameDate, formatGameTitle, formatMemberName, formatTeamLabel } from "../lib/teamsnapHelpers";
|
|
|
|
export function GamePage() {
|
|
const walkup = useWalkupContext();
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const [selectedGameId, setSelectedGameId] = useState(searchParams.get("gameId") ?? "");
|
|
const [clipId, setClipId] = useState<number>(0);
|
|
const [slot, setSlot] = useState<number>(1);
|
|
const [offlineMessage, setOfflineMessage] = useState<string | null>(null);
|
|
const teamId = walkup.selectedTeamId;
|
|
const playerId = walkup.currentPlayerId;
|
|
|
|
useEffect(() => {
|
|
const requestedGameId = searchParams.get("gameId");
|
|
if (requestedGameId) {
|
|
setSelectedGameId(requestedGameId);
|
|
return;
|
|
}
|
|
if (!selectedGameId && walkup.nextGame) {
|
|
setSelectedGameId(String(walkup.nextGame.id));
|
|
}
|
|
}, [searchParams, selectedGameId, walkup.nextGame]);
|
|
|
|
const clipsQuery = useQuery({
|
|
queryKey: ["clips", teamId, playerId],
|
|
queryFn: () => api.listClips(teamId, playerId),
|
|
enabled: Boolean(teamId && playerId),
|
|
});
|
|
const assignmentsQuery = useQuery({
|
|
queryKey: ["assignments", selectedGameId, playerId],
|
|
queryFn: () => api.listAssignments(selectedGameId, playerId),
|
|
enabled: Boolean(selectedGameId && playerId),
|
|
});
|
|
const prepQuery = useQuery({
|
|
queryKey: ["prep", selectedGameId],
|
|
queryFn: () => api.prepareGame(selectedGameId),
|
|
enabled: Boolean(selectedGameId),
|
|
});
|
|
|
|
const saveMutation = useMutation({
|
|
mutationFn: () =>
|
|
api.createAssignment(selectedGameId, {
|
|
external_team_id: teamId,
|
|
external_player_id: playerId,
|
|
clip_id: clipId,
|
|
batting_slot: slot,
|
|
status: "ready",
|
|
}),
|
|
onSuccess: async () => {
|
|
await Promise.all([
|
|
queryClient.invalidateQueries({ queryKey: ["assignments", selectedGameId, playerId] }),
|
|
queryClient.invalidateQueries({ queryKey: ["prep", selectedGameId] }),
|
|
]);
|
|
},
|
|
});
|
|
|
|
function selectGame(gameId: string) {
|
|
setSelectedGameId(gameId);
|
|
setSearchParams({ gameId });
|
|
setOfflineMessage(null);
|
|
}
|
|
|
|
function cachePreparedGame() {
|
|
if (!prepQuery.data) {
|
|
setOfflineMessage("Prepare the game first so there is something to cache locally.");
|
|
return;
|
|
}
|
|
savePreparedGame(selectedGameId, prepQuery.data);
|
|
setOfflineMessage(`Cached ${prepQuery.data.assignments.length} assignments for offline operator use.`);
|
|
}
|
|
|
|
const selectedGame = walkup.games.find((game) => String(game.id) === selectedGameId) ?? null;
|
|
const cachedPrep = selectedGameId ? loadPreparedGame(selectedGameId) : null;
|
|
|
|
if (!walkup.isTeamSnap) {
|
|
return (
|
|
<section className="container-fluid py-4">
|
|
<div className="card shadow-sm">
|
|
<div className="card-body">Reconnect with TeamSnap to attach clips to games.</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
if (!teamId || !playerId) {
|
|
return (
|
|
<section className="container-fluid py-4">
|
|
<div className="card shadow-sm">
|
|
<div className="card-body">
|
|
No player record was found on the selected team, so game-specific clip selection is unavailable.
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<section className="container-fluid py-4 d-grid gap-4">
|
|
<div className="card bg-dark text-white border-0 shadow-sm">
|
|
<div className="card-body p-4 p-lg-5">
|
|
<p className="text-uppercase small text-info-emphasis mb-2">Game clips</p>
|
|
<h1 className="h2">{selectedGame ? formatGameTitle(selectedGame) : "Select a game"}</h1>
|
|
<p className="mb-0 text-white-50">
|
|
{formatMemberName(walkup.currentPlayer)} can attach clips from song files in their own library to any game on{" "}
|
|
{formatTeamLabel(walkup.selectedTeam)}.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="row g-4">
|
|
<div className="col-12 col-xl-6">
|
|
<div className="card shadow-sm h-100">
|
|
<div className="card-body d-grid gap-3">
|
|
<div className="d-grid gap-2">
|
|
<label className="form-label d-grid gap-2">
|
|
<span>Selected game</span>
|
|
<select className="form-select" value={selectedGameId} onChange={(event) => selectGame(event.target.value)}>
|
|
<option value="">Select a game</option>
|
|
{walkup.games.map((game) => (
|
|
<option key={String(game.id)} value={String(game.id)}>
|
|
{formatGameTitle(game)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<div className="text-body-secondary">
|
|
{selectedGame ? formatGameDate(selectedGame) : "Choose a game to attach clips."}
|
|
</div>
|
|
{walkup.nextGame ? <div className="text-body-secondary">Next game: {formatGameTitle(walkup.nextGame)}</div> : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 col-xl-6">
|
|
<div className="card shadow-sm h-100">
|
|
<div className="card-body d-grid gap-3">
|
|
<h2 className="h4 mb-0">Attach a clip</h2>
|
|
{selectedGame ? (
|
|
<>
|
|
<div className="text-body-secondary">{formatGameDate(selectedGame)}</div>
|
|
<label className="form-label d-grid gap-2">
|
|
<span>Clip</span>
|
|
<select className="form-select" value={clipId} onChange={(event) => setClipId(Number(event.target.value))}>
|
|
<option value={0}>Select clip</option>
|
|
{clipsQuery.data?.map((clip) => (
|
|
<option key={clip.id} value={clip.id}>
|
|
{clip.label} from song {clip.asset_title}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label className="form-label d-grid gap-2">
|
|
<span>Suggested batting slot</span>
|
|
<input className="form-control" type="number" value={slot} onChange={(event) => setSlot(Number(event.target.value))} />
|
|
</label>
|
|
<button type="button" className="btn btn-primary" disabled={!clipId} onClick={() => void saveMutation.mutateAsync()}>
|
|
{saveMutation.isPending ? "Saving..." : "Attach clip to this game"}
|
|
</button>
|
|
{saveMutation.error instanceof Error ? <div className="text-body-secondary">{saveMutation.error.message}</div> : null}
|
|
</>
|
|
) : (
|
|
<div className="text-body-secondary">Pick a game to attach clips.</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="row g-4">
|
|
<div className="col-12 col-xl-6">
|
|
<div className="card shadow-sm h-100">
|
|
<div className="card-body d-grid gap-3">
|
|
<h2 className="h4 mb-0">Your selected clips</h2>
|
|
<div className="list-group">
|
|
{assignmentsQuery.data?.map((assignment) => (
|
|
<div className="list-group-item d-flex justify-content-between align-items-center" key={assignment.id}>
|
|
<div>
|
|
<strong>{assignment.clip_label}</strong>
|
|
<div className="text-body-secondary">
|
|
{assignment.asset_title} {assignment.batting_slot ? `| slot ${assignment.batting_slot}` : ""}
|
|
</div>
|
|
</div>
|
|
<span className="badge rounded-pill text-bg-warning">{assignment.status}</span>
|
|
</div>
|
|
))}
|
|
{!assignmentsQuery.isLoading && !assignmentsQuery.data?.length ? (
|
|
<div className="text-body-secondary">No clips attached to this game yet.</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 col-xl-6">
|
|
<div className="card shadow-sm h-100">
|
|
<div className="card-body d-grid gap-3">
|
|
<h2 className="h4 mb-0">Prepared payload</h2>
|
|
<div className="d-grid gap-2">
|
|
<div className="text-body-secondary">Prepared at: {prepQuery.data?.prepared_at ?? "Not prepared yet"}</div>
|
|
<div className="text-body-secondary">Assignments in package: {prepQuery.data?.assignments.length ?? 0}</div>
|
|
<div className="text-body-secondary">Cached locally: {cachedPrep ? `${cachedPrep.assignments.length} assignments` : "No"}</div>
|
|
</div>
|
|
<button type="button" className="btn btn-outline-secondary" onClick={cachePreparedGame} disabled={!selectedGameId}>
|
|
Cache on this device
|
|
</button>
|
|
{offlineMessage ? <div className="text-body-secondary">{offlineMessage}</div> : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|