Files
flxn-app/src/features/bracket/components/match-report.tsx
T
yohlo 0a7b5fa00f
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 8s
CI/CD Pipeline / i18n Catalog Check (push) Failing after 10s
CI/CD Pipeline / Build and Push App Docker Image (push) Skipped
CI/CD Pipeline / Deploy to Kubernetes (push) Skipped
i18n
2026-08-08 15:29:40 -07:00

312 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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";
import { Trans, useLingui } from "@lingui/react/macro";
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 { t } = useLingui();
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(t`Score confirmed 🍻`);
if (data.downstreamReset) {
toast.error(
t`Downstream matches were reset because the correction changed who advances.`
);
}
if (data.groupEditAfterKnockout) {
toast.error(
t`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(
t`Downstream matches were reset because the correction changed who advances.`
);
}
if (data?.groupEditAfterKnockout) {
toast.error(
t`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(t`Score submitted — waiting for the other team`);
},
[report, match.id, formSheet.close, t]
);
const submitConfirm = useCallback(() => {
confirm.mutate({ data: { matchId: match.id } });
formSheet.close();
toast.success(t`Score confirmed 🍻`);
}, [confirm, match.id, formSheet.close, t]);
const submitClear = useCallback(() => {
clear.mutate({ data: { matchId: match.id } });
toast.success(t`Report cleared`);
}, [clear, match.id, t]);
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 ? t`Confirm Score` : t`Report Score`}
{...formSheet.props}
>
{sheetTakenOver ? (
<Stack gap="md">
<Alert
variant="light"
color="yellow"
icon={<LightningIcon size={18} weight="fill" />}
title={t`${teamName(reportingTeam) ?? t`The other team`} just reported a score`}
>
<Trans>Does this look right?</Trans>
</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) ?? t({ message: "Home", context: "match team" })}
</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) ?? t({ message: "Away", context: "match team" })}
</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}
>
<Trans>Confirm {scoreText}</Trans>
</Button>
<Button
fullWidth
variant="default"
onClick={dismissTakeover}
disabled={confirm.isPending}
>
<Trans>Not right enter our score</Trans>
</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}
>
<Trans>Report Score</Trans>
</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 }}>
<Trans>
Waiting for {teamName(opposingTeam) ?? t`the other team`} to confirm{" "}
·{" "}
<Text span fw={600} c="bright">
{scoreText}
</Text>
</Trans>
</Text>
<Button
size={size}
variant="subtle"
color="red"
onClick={submitClear}
loading={clear.isPending}
style={{ flexShrink: 0 }}
>
<Trans>Cancel</Trans>
</Button>
</Flex>
);
}
if (isParticipant) {
return (
<>
<Stack gap={6}>
<Text size="xs" c="dimmed" lineClamp={2}>
<Trans>
{teamName(reportingTeam) ?? t`Opponent`} reported{" "}
<Text span fw={600} c="bright">
{scoreText}
</Text>
</Trans>
</Text>
<Group gap="xs" wrap="nowrap">
<Button
size={size}
leftSection={<CheckIcon size={12} weight="bold" />}
onClick={submitConfirm}
loading={confirm.isPending}
style={{ flex: 1 }}
>
<Trans>Confirm</Trans>
</Button>
<Button
size={size}
variant="default"
onClick={openForm}
disabled={confirm.isPending}
>
<Trans>Not right</Trans>
</Button>
</Group>
</Stack>
{formSheetEl}
</>
);
}
return (
<Text size="xs" c="dimmed" lineClamp={2}>
<Trans>
Pending: {scoreText} · reported by {teamName(reportingTeam) ?? t`a team`}
</Trans>
</Text>
);
}
return null;
};