social overhaul
This commit is contained in:
@@ -16,9 +16,10 @@ interface BracketViewProps {
|
||||
};
|
||||
renderMatch?: (match: Match) => React.ReactNode;
|
||||
bottomOffset?: number;
|
||||
nextUpMatchId?: string;
|
||||
}
|
||||
|
||||
const BracketView: React.FC<BracketViewProps> = ({ bracket, showControls, groupConfig, renderMatch, bottomOffset }) => {
|
||||
const BracketView: React.FC<BracketViewProps> = ({ bracket, showControls, groupConfig, renderMatch, bottomOffset, nextUpMatchId }) => {
|
||||
const height = useAppShellHeight();
|
||||
const viewportRef = useRef<HTMLDivElement>(null);
|
||||
const hasAutoScrolled = useRef(false);
|
||||
@@ -50,9 +51,10 @@ const BracketView: React.FC<BracketViewProps> = ({ bracket, showControls, groupC
|
||||
const matches = [...bracket.winners.flat(), ...bracket.losers.flat()].filter(
|
||||
(match) => !match.bye
|
||||
);
|
||||
const target =
|
||||
matches.find((match) => match.status === "started") ??
|
||||
matches.find((match) => match.status === "ready");
|
||||
const target = nextUpMatchId !== undefined
|
||||
? matches.find((match) => match.id === nextUpMatchId)
|
||||
: matches.find((match) => match.status === "started") ??
|
||||
matches.find((match) => match.status === "ready");
|
||||
if (!target) return;
|
||||
|
||||
const viewport = viewportRef.current;
|
||||
@@ -78,7 +80,7 @@ const BracketView: React.FC<BracketViewProps> = ({ bracket, showControls, groupC
|
||||
? "auto"
|
||||
: "smooth",
|
||||
});
|
||||
}, [bracket]);
|
||||
}, [bracket, nextUpMatchId]);
|
||||
|
||||
return <Box pos="relative">
|
||||
<ScrollArea
|
||||
@@ -95,14 +97,14 @@ const BracketView: React.FC<BracketViewProps> = ({ bracket, showControls, groupC
|
||||
<Text fw={600} size="md" m={16}>
|
||||
Winners Bracket
|
||||
</Text>
|
||||
<Bracket rounds={bracket.winners} orders={orders} showControls={showControls} groupConfig={groupConfig} renderMatch={renderMatch} onMatchTap={handleMatchTap} selectedMatchLid={selectedLid} />
|
||||
<Bracket rounds={bracket.winners} orders={orders} showControls={showControls} groupConfig={groupConfig} renderMatch={renderMatch} onMatchTap={handleMatchTap} selectedMatchLid={selectedLid} nextUpMatchId={nextUpMatchId} />
|
||||
</div>
|
||||
{bracket.losers && bracket.losers.length > 0 && bracket.losers.some(round => round.length > 0) && (
|
||||
<div>
|
||||
<Text fw={600} size="md" m={16}>
|
||||
Losers Bracket
|
||||
</Text>
|
||||
<Bracket rounds={bracket.losers} orders={orders} showControls={showControls} groupConfig={groupConfig} renderMatch={renderMatch} onMatchTap={handleMatchTap} selectedMatchLid={selectedLid} />
|
||||
<Bracket rounds={bracket.losers} orders={orders} showControls={showControls} groupConfig={groupConfig} renderMatch={renderMatch} onMatchTap={handleMatchTap} selectedMatchLid={selectedLid} nextUpMatchId={nextUpMatchId} />
|
||||
</div>
|
||||
)}
|
||||
{bottomOffset ? <div style={{ height: bottomOffset }} /> : null}
|
||||
|
||||
@@ -14,6 +14,7 @@ interface BracketProps {
|
||||
renderMatch?: (match: Match) => React.ReactNode;
|
||||
onMatchTap?: (match: Match) => void;
|
||||
selectedMatchLid?: number | null;
|
||||
nextUpMatchId?: string;
|
||||
}
|
||||
|
||||
export const Bracket: React.FC<BracketProps> = ({
|
||||
@@ -24,6 +25,7 @@ export const Bracket: React.FC<BracketProps> = ({
|
||||
renderMatch,
|
||||
onMatchTap,
|
||||
selectedMatchLid,
|
||||
nextUpMatchId,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
@@ -149,6 +151,7 @@ export const Bracket: React.FC<BracketProps> = ({
|
||||
groupConfig={groupConfig}
|
||||
onTap={onMatchTap}
|
||||
selected={selectedMatchLid === match.lid}
|
||||
nextUpMatchId={nextUpMatchId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ import TeamAvatar from "@/components/team-avatar";
|
||||
import AnimatedScore from "@/features/matches/components/animated-score";
|
||||
import EmojiBar from "@/features/reactions/components/emoji-bar";
|
||||
import TeamHeadToHeadSheet from "@/features/matches/components/team-head-to-head-sheet";
|
||||
import { MatchReport } from "./match-report";
|
||||
import Sheet from "@/components/sheet/sheet";
|
||||
import { useSheet } from "@/hooks/use-sheet";
|
||||
|
||||
@@ -149,6 +150,8 @@ const MatchDock = ({ match, onClose }: MatchDockProps) => {
|
||||
ended={ended}
|
||||
/>
|
||||
|
||||
<MatchReport match={match} />
|
||||
|
||||
<Group justify="space-between" wrap="nowrap" gap="sm">
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Suspense
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { Alert, Button, Flex, Group, Paper, Stack, Text } from "@mantine/core";
|
||||
import { CheckIcon, LightningIcon } from "@phosphor-icons/react";
|
||||
import { Match } from "@/features/matches/types";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import {
|
||||
useReportMatchScore,
|
||||
useConfirmMatchScore,
|
||||
useClearMatchReport,
|
||||
} from "@/features/matches/queries";
|
||||
import { useSheet } from "@/hooks/use-sheet";
|
||||
import Sheet from "@/components/sheet/sheet";
|
||||
import { MatchForm } from "./match-form";
|
||||
import toast from "@/lib/sonner";
|
||||
|
||||
const teamId = (t: Match["home"]): string | undefined =>
|
||||
!t ? undefined : typeof t === "string" ? t : t.id;
|
||||
|
||||
const teamPlayerIds = (t: Match["home"]): string[] =>
|
||||
t && typeof t !== "string" ? (t.players ?? []).map((p) => p.id) : [];
|
||||
|
||||
const teamName = (t: Match["home"]): string | undefined =>
|
||||
t && typeof t !== "string" ? t.name : undefined;
|
||||
|
||||
interface MatchReportProps {
|
||||
match: Match;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export const MatchReport: React.FC<MatchReportProps> = ({ match, compact }) => {
|
||||
const { user } = useAuth();
|
||||
const formSheet = useSheet();
|
||||
const [acknowledgedReport, setAcknowledgedReport] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const homeId = teamId(match.home);
|
||||
const onHome = !!user?.id && teamPlayerIds(match.home).includes(user.id);
|
||||
const onAway = !!user?.id && teamPlayerIds(match.away).includes(user.id);
|
||||
const isParticipant = onHome || onAway;
|
||||
|
||||
const hasPending =
|
||||
match.reported_by_team != null &&
|
||||
match.reported_home_cups != null &&
|
||||
match.reported_away_cups != null;
|
||||
|
||||
const reportedByHome = hasPending && match.reported_by_team === homeId;
|
||||
const reportingTeam = hasPending
|
||||
? reportedByHome
|
||||
? match.home
|
||||
: match.away
|
||||
: undefined;
|
||||
const opposingTeam = reportedByHome ? match.away : match.home;
|
||||
const reportedByMine =
|
||||
hasPending && ((reportedByHome && onHome) || (!reportedByHome && onAway));
|
||||
|
||||
const pendingSignature = hasPending
|
||||
? [
|
||||
match.reported_by_team,
|
||||
match.reported_home_cups,
|
||||
match.reported_away_cups,
|
||||
match.reported_ot_count ?? 0,
|
||||
].join(":")
|
||||
: null;
|
||||
|
||||
const sheetTakenOver =
|
||||
formSheet.isOpen &&
|
||||
isParticipant &&
|
||||
hasPending &&
|
||||
!reportedByMine &&
|
||||
pendingSignature !== acknowledgedReport;
|
||||
|
||||
const report = useReportMatchScore(user?.id, {
|
||||
onSuccess: (data: any) => {
|
||||
if (data?.finalized) {
|
||||
toast.success("Score confirmed 🍻");
|
||||
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."
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const confirm = useConfirmMatchScore({
|
||||
onSuccess: (data: any) => {
|
||||
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."
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const clear = useClearMatchReport();
|
||||
|
||||
const submitReport = useCallback(
|
||||
(data: { home_cups: number; away_cups: number; ot_count: number }) => {
|
||||
report.mutate({ data: { ...data, matchId: match.id } });
|
||||
formSheet.close();
|
||||
toast.success("Score submitted — waiting for the other team");
|
||||
},
|
||||
[report, match.id, formSheet.close]
|
||||
);
|
||||
|
||||
const submitConfirm = useCallback(() => {
|
||||
confirm.mutate({ data: { matchId: match.id } });
|
||||
formSheet.close();
|
||||
toast.success("Score confirmed 🍻");
|
||||
}, [confirm, match.id, formSheet.close]);
|
||||
|
||||
const submitClear = useCallback(() => {
|
||||
clear.mutate({ data: { matchId: match.id } });
|
||||
toast.success("Report cleared");
|
||||
}, [clear, match.id]);
|
||||
|
||||
const openForm = useCallback(() => {
|
||||
setAcknowledgedReport(pendingSignature);
|
||||
formSheet.open();
|
||||
}, [pendingSignature, formSheet.open]);
|
||||
|
||||
const dismissTakeover = useCallback(() => {
|
||||
setAcknowledgedReport(pendingSignature);
|
||||
}, [pendingSignature]);
|
||||
|
||||
if (match.status !== "started") return null;
|
||||
|
||||
const scoreText = hasPending
|
||||
? `${match.reported_home_cups}–${match.reported_away_cups}`
|
||||
: "";
|
||||
const size = compact ? "compact-xs" : "sm";
|
||||
|
||||
const formSheetEl = (
|
||||
<Sheet
|
||||
title={sheetTakenOver ? "Confirm Score" : "Report Score"}
|
||||
{...formSheet.props}
|
||||
>
|
||||
{sheetTakenOver ? (
|
||||
<Stack gap="md">
|
||||
<Alert
|
||||
variant="light"
|
||||
color="yellow"
|
||||
icon={<LightningIcon size={18} weight="fill" />}
|
||||
title={`${teamName(reportingTeam) ?? "The other team"} just reported a score`}
|
||||
>
|
||||
Does this look right?
|
||||
</Alert>
|
||||
|
||||
<Paper withBorder p="md">
|
||||
<Group justify="center" gap="md" wrap="nowrap">
|
||||
<Stack gap={2} align="center" style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text size="sm" fw={600} ta="center" lineClamp={1}>
|
||||
{teamName(match.home) ?? "Home"}
|
||||
</Text>
|
||||
<Text fz={34} fw={700} lh={1.2}>
|
||||
{match.reported_home_cups}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text c="dimmed" fw={600}>
|
||||
–
|
||||
</Text>
|
||||
<Stack gap={2} align="center" style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text size="sm" fw={600} ta="center" lineClamp={1}>
|
||||
{teamName(match.away) ?? "Away"}
|
||||
</Text>
|
||||
<Text fz={34} fw={700} lh={1.2}>
|
||||
{match.reported_away_cups}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
<Stack gap="xs">
|
||||
<Button
|
||||
fullWidth
|
||||
leftSection={<CheckIcon size={14} weight="bold" />}
|
||||
onClick={submitConfirm}
|
||||
loading={confirm.isPending}
|
||||
>
|
||||
Confirm {scoreText}
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="default"
|
||||
onClick={dismissTakeover}
|
||||
disabled={confirm.isPending}
|
||||
>
|
||||
Not right — enter our score
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
) : (
|
||||
<MatchForm
|
||||
match={match}
|
||||
onSubmit={submitReport}
|
||||
onCancel={formSheet.close}
|
||||
loading={report.isPending}
|
||||
/>
|
||||
)}
|
||||
</Sheet>
|
||||
);
|
||||
|
||||
if (isParticipant && !hasPending) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size={size}
|
||||
fullWidth={!compact}
|
||||
onClick={openForm}
|
||||
loading={report.isPending}
|
||||
>
|
||||
Report Score
|
||||
</Button>
|
||||
{formSheetEl}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasPending) {
|
||||
if (reportedByMine) {
|
||||
return (
|
||||
<Flex
|
||||
direction={compact ? "column" : "row"}
|
||||
align={compact ? "stretch" : "center"}
|
||||
justify="space-between"
|
||||
gap={compact ? 4 : "xs"}
|
||||
>
|
||||
<Text size="xs" c="dimmed" lineClamp={2} style={{ minWidth: 0 }}>
|
||||
Waiting for {teamName(opposingTeam) ?? "the other team"} to confirm{" "}
|
||||
·{" "}
|
||||
<Text span fw={600} c="bright">
|
||||
{scoreText}
|
||||
</Text>
|
||||
</Text>
|
||||
<Button
|
||||
size={size}
|
||||
variant="subtle"
|
||||
color="red"
|
||||
onClick={submitClear}
|
||||
loading={clear.isPending}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
if (isParticipant) {
|
||||
return (
|
||||
<>
|
||||
<Stack gap={6}>
|
||||
<Text size="xs" c="dimmed" lineClamp={2}>
|
||||
{teamName(reportingTeam) ?? "Opponent"} reported{" "}
|
||||
<Text span fw={600} c="bright">
|
||||
{scoreText}
|
||||
</Text>
|
||||
</Text>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Button
|
||||
size={size}
|
||||
leftSection={<CheckIcon size={12} weight="bold" />}
|
||||
onClick={submitConfirm}
|
||||
loading={confirm.isPending}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
<Button
|
||||
size={size}
|
||||
variant="default"
|
||||
onClick={openForm}
|
||||
disabled={confirm.isPending}
|
||||
>
|
||||
Not right
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
{formSheetEl}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text size="xs" c="dimmed" lineClamp={2}>
|
||||
Pending: {scoreText} · reported by {teamName(reportingTeam) ?? "a team"}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
Reference in New Issue
Block a user