social overhaul

This commit is contained in:
yohlo
2026-07-22 15:47:34 -07:00
parent 9db3fa697d
commit 86cda294f9
45 changed files with 3026 additions and 430 deletions
+169 -138
View File
@@ -1,4 +1,4 @@
import { ActionIcon, Card, Flex, Text, Indicator } from "@mantine/core";
import { ActionIcon, Card, Flex, Text, Indicator, Stack } from "@mantine/core";
import { PlayIcon, PencilIcon, SpeakerHighIcon } from "@phosphor-icons/react";
import React, { useCallback, useMemo } from "react";
import { MatchSlot } from "./match-slot";
@@ -6,6 +6,8 @@ import { Match } from "@/features/matches/types";
import { Team } from "@/features/teams/types";
import { useSheet } from "@/hooks/use-sheet";
import { MatchForm } from "./match-form";
import { MatchReport } from "./match-report";
import toast from "@/lib/sonner";
import Sheet from "@/components/sheet/sheet";
import { useServerMutation } from "@/lib/tanstack-query/hooks";
import { endMatch, startMatch } from "@/features/matches/server";
@@ -25,6 +27,7 @@ interface MatchCardProps {
};
onTap?: (match: Match) => void;
selected?: boolean;
nextUpMatchId?: string;
}
export const MatchCard: React.FC<MatchCardProps> = ({
@@ -34,6 +37,7 @@ export const MatchCard: React.FC<MatchCardProps> = ({
groupConfig,
onTap,
selected,
nextUpMatchId,
}) => {
const queryClient = useQueryClient();
const editSheet = useSheet();
@@ -41,6 +45,8 @@ export const MatchCard: React.FC<MatchCardProps> = ({
const canTap = !!(onTap && match.home && match.away);
const isNextUp = nextUpMatchId !== undefined && match.id === nextUpMatchId;
const homeSlot = useMemo(
() => ({
from: orders[match.home_from_lid],
@@ -80,7 +86,8 @@ export const MatchCard: React.FC<MatchCardProps> = ({
);
const showEditButton = useMemo(
() => showControls && match.status === "started",
() =>
showControls && (match.status === "started" || match.status === "ended"),
[showControls, match.status]
);
@@ -98,11 +105,21 @@ export const MatchCard: React.FC<MatchCardProps> = ({
const end = useServerMutation({
mutationFn: endMatch,
onSuccess: () => {
onSuccess: (data) => {
queryClient.invalidateQueries({
queryKey: tournamentKeys.details(match.tournament.id),
});
editSheet.close();
if (data?.downstreamReset) {
toast.error(
"Downstream matches were reset because the correction changed who advances."
);
}
if (data?.groupEditAfterKnockout) {
toast.error(
"Group result changed after the bracket was seeded — reseed the knockout stage if needed."
);
}
},
});
@@ -195,10 +212,8 @@ export const MatchCard: React.FC<MatchCardProps> = ({
data: match.id,
});
// Skip announcements for regional tournaments
const isRegional = match.tournament?.regional === true;
// Play walkout sequence after starting the match (only for non-regional tournaments)
if (!isRegional && hasWalkoutData && match.home?.name && match.away?.name) {
try {
const homeTeam = match.home as Team;
@@ -217,152 +232,168 @@ export const MatchCard: React.FC<MatchCardProps> = ({
}, [match, start, hasWalkoutData, playTeamWalkout, speak]);
return (
<Flex direction="row" align="center" justify="end" gap={8}>
<Text
c="dimmed"
fw="bolder"
px={6}
py={2}
style={{
backgroundColor: 'var(--mantine-color-body)'
}}
>
{match.order}
</Text>
<Flex align="stretch">
<Indicator
inline
processing={match.status === "started"}
color="red"
size={12}
disabled={match.status !== "started" || showEditButton}
<Stack gap={6} align="flex-end">
<Flex direction="row" align="center" justify="end" gap={8}>
<Text
c="dimmed"
fw="bolder"
px={6}
py={2}
style={{
backgroundColor: 'var(--mantine-color-body)'
}}
>
<Card
w={showToolbar || showEditButton ? 200 : 220}
withBorder
pos="relative"
className={canTap ? styles["tappable-card"] : undefined}
onClick={canTap ? () => onTap!(match) : undefined}
role={canTap ? "button" : undefined}
tabIndex={canTap ? 0 : undefined}
onKeyDown={
canTap
? (e: React.KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onTap!(match);
}
}
: undefined
}
aria-label={
canTap
? `Match ${match.order}: ${match.home!.name} vs ${match.away!.name}`
: undefined
}
style={{
overflow: "visible",
backgroundColor: 'var(--mantine-color-body)',
borderColor: selected
? 'var(--mantine-primary-color-filled)'
: 'var(--mantine-color-default-border)',
boxShadow: 'var(--mantine-shadow-sm)',
}}
data-match-lid={match.lid}
{match.order}
</Text>
<Flex
align="stretch"
style={{
borderRadius: "var(--mantine-radius-default)",
boxShadow: isNextUp
? "0 0 0 1px var(--mantine-primary-color-filled), 0 0 12px var(--mantine-primary-color-light-hover)"
: undefined,
transition:
"box-shadow 200ms cubic-bezier(0.32, 0.72, 0, 1)",
}}
>
<Indicator
inline
processing={match.status === "started"}
color="red"
size={12}
disabled={match.status !== "started" || showEditButton}
>
<Card.Section withBorder p={0}>
<MatchSlot {...homeSlot} />
</Card.Section>
<Card
w={showToolbar || showEditButton ? 200 : 220}
withBorder
pos="relative"
className={canTap ? styles["tappable-card"] : undefined}
onClick={canTap ? () => onTap!(match) : undefined}
role={canTap ? "button" : undefined}
tabIndex={canTap ? 0 : undefined}
onKeyDown={
canTap
? (e: React.KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onTap!(match);
}
}
: undefined
}
aria-label={
canTap
? `Match ${match.order}: ${match.home!.name} vs ${match.away!.name}`
: undefined
}
style={{
overflow: "visible",
backgroundColor: 'var(--mantine-color-body)',
borderColor: selected
? 'var(--mantine-primary-color-filled)'
: 'var(--mantine-color-default-border)',
boxShadow: 'var(--mantine-shadow-sm)',
transition:
'border-color 200ms cubic-bezier(0.32, 0.72, 0, 1)',
}}
data-match-lid={match.lid}
>
<Card.Section withBorder p={0}>
<MatchSlot {...homeSlot} />
</Card.Section>
<Card.Section p={0} mb={-16}>
<MatchSlot {...awaySlot} />
</Card.Section>
<Card.Section p={0} mb={-16}>
<MatchSlot {...awaySlot} />
</Card.Section>
{match.reset && (
<Text
pos="absolute"
top={-20}
left={8}
size="xs"
c="dimmed"
fw="bold"
>
* If necessary
</Text>
)}
{match.reset && (
<Text
pos="absolute"
top={-20}
left={8}
size="xs"
c="dimmed"
fw="bold"
>
* If necessary
</Text>
)}
{showControls && match.status !== "tbd" && (
{showControls && match.status !== "tbd" && (
<ActionIcon
pos="absolute"
bottom={-2}
left={-26}
size="sm"
variant="subtle"
color="gray"
onClick={(e) => {
e.stopPropagation();
handleSpeakerClick();
}}
aria-label="Announce matchup"
>
<SpeakerHighIcon size={12} />
</ActionIcon>
)}
</Card>
</Indicator>
{showToolbar && (
<Flex direction="column" justify="center" align="center">
<ActionIcon
pos="absolute"
bottom={-2}
left={-26}
color="green"
onClick={handleStart}
loading={start.isPending}
size="sm"
variant="subtle"
color="gray"
onClick={(e) => {
e.stopPropagation();
handleSpeakerClick();
h="100%"
radius="sm"
ml={-4}
aria-label="Start match"
style={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}
aria-label="Announce matchup"
>
<SpeakerHighIcon size={12} />
<PlayIcon size={14} />
</ActionIcon>
)}
</Card>
</Indicator>
{showToolbar && (
<Flex direction="column" justify="center" align="center">
<ActionIcon
color="green"
onClick={handleStart}
loading={start.isPending}
size="sm"
h="100%"
radius="sm"
ml={-4}
aria-label="Start match"
style={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}
>
<PlayIcon size={14} />
</ActionIcon>
</Flex>
)}
</Flex>
)}
{showEditButton && (
<Flex direction="column" justify="center" align="center">
<ActionIcon
color="blue"
onClick={editSheet.open}
loading={end.isPending}
size="sm"
h="100%"
radius="sm"
ml={-4}
aria-label="Edit match score"
style={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}
>
<PencilIcon size={14} />
</ActionIcon>
</Flex>
)}
{showEditButton && (
<Flex direction="column" justify="center" align="center">
<ActionIcon
color={match.status === "ended" ? "gray" : "blue"}
onClick={editSheet.open}
loading={end.isPending}
size="sm"
h="100%"
radius="sm"
ml={-4}
aria-label="Edit match score"
style={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}
>
<PencilIcon size={14} />
</ActionIcon>
</Flex>
)}
</Flex>
<Sheet title="Edit Match" {...editSheet.props}>
<MatchForm
match={match}
onSubmit={handleFormSubmit}
onCancel={editSheet.close}
loading={end.isPending}
/>
</Sheet>
</Flex>
<Sheet title="Edit Match" {...editSheet.props}>
<MatchForm
match={match}
onSubmit={handleFormSubmit}
onCancel={editSheet.close}
loading={end.isPending}
/>
</Sheet>
</Flex>
{!showControls && <MatchReport match={match} compact />}
</Stack>
);
};