play walkout songs
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { ActionIcon, Card, Flex, Text, Stack, Indicator } from "@mantine/core";
|
||||
import { ActionIcon, Card, Flex, Text, Indicator } from "@mantine/core";
|
||||
import { PlayIcon, PencilIcon, SpeakerHighIcon } from "@phosphor-icons/react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { MatchSlot } from "./match-slot";
|
||||
import { Match } from "@/features/matches/types";
|
||||
import { Team } from "@/features/teams/types";
|
||||
import { useSheet } from "@/hooks/use-sheet";
|
||||
import { MatchForm } from "./match-form";
|
||||
import Sheet from "@/components/sheet/sheet";
|
||||
@@ -10,6 +11,7 @@ import { useServerMutation } from "@/lib/tanstack-query/hooks";
|
||||
import { endMatch, startMatch } from "@/features/matches/server";
|
||||
import { tournamentKeys } from "@/features/tournaments/queries";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useSpotifyPlayback } from "@/lib/spotify/hooks";
|
||||
|
||||
interface MatchCardProps {
|
||||
match: Match;
|
||||
@@ -24,6 +26,7 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
||||
}) => {
|
||||
const queryClient = useQueryClient();
|
||||
const editSheet = useSheet();
|
||||
const { playTrack, pause } = useSpotifyPlayback();
|
||||
const homeSlot = useMemo(
|
||||
() => ({
|
||||
from: orders[match.home_from_lid],
|
||||
@@ -65,6 +68,8 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
||||
[showControls, match.status]
|
||||
);
|
||||
|
||||
const hasWalkoutData = showControls && match.home && match.away && 'song_id' in match.home && 'song_id' in match.away;
|
||||
|
||||
const start = useServerMutation({
|
||||
mutationFn: startMatch,
|
||||
successMessage: "Match started!",
|
||||
@@ -84,19 +89,13 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
||||
},
|
||||
});
|
||||
|
||||
const handleStart = useCallback(async () => {
|
||||
await start.mutate({
|
||||
data: match.id,
|
||||
});
|
||||
}, [match]);
|
||||
|
||||
const handleFormSubmit = useCallback(
|
||||
async (data: {
|
||||
home_cups: number;
|
||||
away_cups: number;
|
||||
ot_count: number;
|
||||
}) => {
|
||||
await end.mutate({
|
||||
end.mutate({
|
||||
data: {
|
||||
...data,
|
||||
matchId: match.id,
|
||||
@@ -107,12 +106,14 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
||||
[match.id, editSheet]
|
||||
);
|
||||
|
||||
const handleSpeakerClick = useCallback(() => {
|
||||
if ("speechSynthesis" in window && match.home?.name && match.away?.name) {
|
||||
const utterance = new SpeechSynthesisUtterance(
|
||||
`${match.home.name} vs. ${match.away.name}`
|
||||
);
|
||||
const speak = useCallback((text: string): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
if (!("speechSynthesis" in window)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const utterance = new SpeechSynthesisUtterance(text);
|
||||
const voices = window.speechSynthesis.getVoices();
|
||||
|
||||
const preferredVoice =
|
||||
@@ -130,9 +131,71 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
||||
utterance.volume = 0.8;
|
||||
utterance.pitch = 1.0;
|
||||
|
||||
utterance.onend = () => resolve();
|
||||
utterance.onerror = () => resolve();
|
||||
|
||||
window.speechSynthesis.speak(utterance);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const playTeamWalkout = useCallback((team: Team): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
const songDuration = (team.song_end - team.song_start) * 1000;
|
||||
|
||||
playTrack(team.song_id, undefined, team.song_start * 1000);
|
||||
|
||||
setTimeout(async () => {
|
||||
await pause();
|
||||
resolve();
|
||||
}, songDuration);
|
||||
});
|
||||
}, [playTrack, pause]);
|
||||
|
||||
const handleSpeakerClick = useCallback(async () => {
|
||||
if (!hasWalkoutData || !match.home?.name || !match.away?.name) {
|
||||
await speak(`${match.home?.name || "Home"} vs. ${match.away?.name || "Away"}`);
|
||||
return;
|
||||
}
|
||||
}, [match.home?.name, match.away?.name]);
|
||||
|
||||
try {
|
||||
const homeTeam = match.home as Team;
|
||||
const awayTeam = match.away as Team;
|
||||
|
||||
await playTeamWalkout(homeTeam);
|
||||
await speak(homeTeam.name);
|
||||
await speak("versus");
|
||||
await playTeamWalkout(awayTeam);
|
||||
await speak(awayTeam.name);
|
||||
await speak("have fun, good luck!");
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Walkout sequence error:', error);
|
||||
await speak(`${match.home.name} vs. ${match.away.name}`);
|
||||
}
|
||||
}, [hasWalkoutData, match.home, match.away, speak, playTeamWalkout]);
|
||||
|
||||
const handleStart = useCallback(async () => {
|
||||
start.mutate({
|
||||
data: match.id,
|
||||
});
|
||||
|
||||
// Play walkout sequence after starting the match
|
||||
if (hasWalkoutData && match.home?.name && match.away?.name) {
|
||||
try {
|
||||
const homeTeam = match.home as Team;
|
||||
const awayTeam = match.away as Team;
|
||||
|
||||
await playTeamWalkout(homeTeam);
|
||||
await speak(homeTeam.name);
|
||||
await speak("versus");
|
||||
await playTeamWalkout(awayTeam);
|
||||
await speak(awayTeam.name);
|
||||
await speak("have fun, good luck!");
|
||||
} catch (error) {
|
||||
console.warn('Auto-walkout sequence error:', error);
|
||||
}
|
||||
}
|
||||
}, [match, start, hasWalkoutData, playTeamWalkout, speak]);
|
||||
|
||||
return (
|
||||
<Flex direction="row" align="center" justify="end" gap={8}>
|
||||
@@ -175,7 +238,7 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{showControls && (
|
||||
{showControls && match.status !== "tbd" && (
|
||||
<ActionIcon
|
||||
pos="absolute"
|
||||
bottom={-2}
|
||||
@@ -210,6 +273,7 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
|
||||
{showEditButton && (
|
||||
<Flex direction="column" justify="center" align="center">
|
||||
<ActionIcon
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { TeamInfo } from "../teams/types";
|
||||
import { TeamInfo, Team } from "../teams/types";
|
||||
import { TournamentInfo } from "../tournaments/types";
|
||||
|
||||
export type MatchStatus = "tbd" | "ready" | "started" | "ended";
|
||||
@@ -23,8 +23,8 @@ export interface Match {
|
||||
is_losers_bracket: boolean;
|
||||
status: MatchStatus;
|
||||
tournament: TournamentInfo;
|
||||
home?: TeamInfo;
|
||||
away?: TeamInfo;
|
||||
home?: TeamInfo | Team;
|
||||
away?: TeamInfo | Team;
|
||||
created: string;
|
||||
updated: string;
|
||||
home_seed?: number;
|
||||
|
||||
@@ -32,9 +32,10 @@ export const updateTournament = createServerFn()
|
||||
export const getTournament = createServerFn()
|
||||
.validator(z.string())
|
||||
.middleware([superTokensFunctionMiddleware])
|
||||
.handler(async ({ data: tournamentId }) =>
|
||||
toServerResult(() => pbAdmin.getTournament(tournamentId))
|
||||
);
|
||||
.handler(async ({ data: tournamentId, context }) => {
|
||||
const isAdmin = context.roles.includes("Admin");
|
||||
return toServerResult(() => pbAdmin.getTournament(tournamentId, isAdmin));
|
||||
});
|
||||
|
||||
export const getCurrentTournament = createServerFn()
|
||||
.middleware([superTokensFunctionMiddleware])
|
||||
|
||||
Reference in New Issue
Block a user