223 lines
6.9 KiB
TypeScript
223 lines
6.9 KiB
TypeScript
import { Suspense } from "react";
|
|
import {
|
|
ActionIcon,
|
|
Box,
|
|
Center,
|
|
CloseButton,
|
|
Group,
|
|
Indicator,
|
|
Loader,
|
|
Paper,
|
|
Stack,
|
|
Text,
|
|
Tooltip,
|
|
} from "@mantine/core";
|
|
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
|
import { FootballHelmetIcon } from "@phosphor-icons/react";
|
|
import { useNavigate } from "@tanstack/react-router";
|
|
import { Match } from "@/features/matches/types";
|
|
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";
|
|
|
|
const EASE: [number, number, number, number] = [0.32, 0.72, 0, 1];
|
|
|
|
interface MatchDockProps {
|
|
match: Match | null;
|
|
onClose: () => void;
|
|
}
|
|
|
|
const TeamRow = ({
|
|
team,
|
|
cups,
|
|
isWinner,
|
|
isRegional,
|
|
ended,
|
|
}: {
|
|
team: NonNullable<Match["home"]>;
|
|
cups: number;
|
|
isWinner: boolean;
|
|
isRegional: boolean;
|
|
ended: boolean;
|
|
}) => {
|
|
const navigate = useNavigate();
|
|
|
|
return (
|
|
<Group justify="space-between" wrap="nowrap" gap="sm">
|
|
<Group
|
|
gap="sm"
|
|
wrap="nowrap"
|
|
style={{ flex: 1, minWidth: 0, cursor: "pointer" }}
|
|
onClick={() => navigate({ to: `/teams/${team.id}` })}
|
|
>
|
|
<TeamAvatar
|
|
team={team}
|
|
size={32}
|
|
radius="sm"
|
|
winner={ended && isWinner}
|
|
isRegional={isRegional}
|
|
/>
|
|
<Text size="sm" fw={ended && isWinner ? 700 : 500} lineClamp={1}>
|
|
{team.name}
|
|
</Text>
|
|
</Group>
|
|
{ended && (
|
|
<AnimatedScore
|
|
value={cups}
|
|
size="lg"
|
|
fw={700}
|
|
c={isWinner ? "green" : "dimmed"}
|
|
style={{ minWidth: 28, textAlign: "center" }}
|
|
/>
|
|
)}
|
|
</Group>
|
|
);
|
|
};
|
|
|
|
const MatchDock = ({ match, onClose }: MatchDockProps) => {
|
|
const reduceMotion = useReducedMotion();
|
|
const h2hSheet = useSheet();
|
|
const hasPrivate = match?.home?.private || match?.away?.private;
|
|
const ended = match?.status === "ended";
|
|
const started = match?.status === "started";
|
|
|
|
return (
|
|
<>
|
|
<AnimatePresence>
|
|
{match && match.home && match.away && (
|
|
<motion.div
|
|
key="match-dock"
|
|
initial={reduceMotion ? { opacity: 0 } : { opacity: 0, y: 16 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={reduceMotion ? { opacity: 0 } : { opacity: 0, y: 16 }}
|
|
transition={{ duration: 0.2, ease: EASE }}
|
|
style={{
|
|
position: "absolute",
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
zIndex: 5,
|
|
padding: "0 12px 12px",
|
|
}}
|
|
>
|
|
<Paper withBorder shadow="md" radius="lg" px="md" py="sm">
|
|
<Stack gap="xs">
|
|
<Group justify="space-between" wrap="nowrap">
|
|
<Group gap="xs" wrap="nowrap">
|
|
{started && (
|
|
<Indicator
|
|
size={8}
|
|
color="red"
|
|
processing
|
|
position="middle-start"
|
|
offset={0}
|
|
/>
|
|
)}
|
|
<Text size="xs" fw={600} c="dimmed" lineClamp={1}>
|
|
Match {match.order} · Round {match.round + 1}
|
|
{match.is_losers_bracket && " (Losers)"}
|
|
</Text>
|
|
</Group>
|
|
<CloseButton
|
|
size="sm"
|
|
onClick={onClose}
|
|
aria-label="Close match actions"
|
|
/>
|
|
</Group>
|
|
|
|
<TeamRow
|
|
team={match.home}
|
|
cups={match.home_cups}
|
|
isWinner={match.home_cups > match.away_cups}
|
|
isRegional={match.tournament.regional === true}
|
|
ended={ended}
|
|
/>
|
|
<Box
|
|
style={{
|
|
height: 1,
|
|
backgroundColor: "var(--mantine-color-default-border)",
|
|
}}
|
|
/>
|
|
<TeamRow
|
|
team={match.away}
|
|
cups={match.away_cups}
|
|
isWinner={match.away_cups > match.home_cups}
|
|
isRegional={match.tournament.regional === true}
|
|
ended={ended}
|
|
/>
|
|
|
|
<MatchReport match={match} />
|
|
|
|
<Group justify="space-between" wrap="nowrap" gap="sm">
|
|
<Box style={{ flex: 1, minWidth: 0 }}>
|
|
<Suspense
|
|
fallback={
|
|
<Center py={4}>
|
|
<Loader size="xs" />
|
|
</Center>
|
|
}
|
|
>
|
|
<EmojiBar matchId={match.id} />
|
|
</Suspense>
|
|
</Box>
|
|
{!hasPrivate && (
|
|
<Tooltip label="Head to Head" withArrow position="top">
|
|
<ActionIcon
|
|
variant="subtle"
|
|
size="sm"
|
|
onClick={h2hSheet.open}
|
|
aria-label="View head-to-head"
|
|
w={40}
|
|
style={{ flexShrink: 0 }}
|
|
>
|
|
<Group
|
|
style={{ position: "relative", width: 27.5, height: 16 }}
|
|
>
|
|
<FootballHelmetIcon
|
|
size={14}
|
|
style={{
|
|
position: "absolute",
|
|
left: 0,
|
|
top: 0,
|
|
transform: "rotate(25deg)",
|
|
}}
|
|
/>
|
|
<FootballHelmetIcon
|
|
size={14}
|
|
style={{
|
|
position: "absolute",
|
|
right: 0,
|
|
top: 0,
|
|
transform: "scaleX(-1) rotate(25deg)",
|
|
}}
|
|
/>
|
|
</Group>
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
)}
|
|
</Group>
|
|
</Stack>
|
|
</Paper>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{match?.home && match?.away && h2hSheet.isOpen && (
|
|
<Sheet title="Head to Head" {...h2hSheet.props}>
|
|
<TeamHeadToHeadSheet
|
|
team1={match.home}
|
|
team2={match.away}
|
|
isOpen={h2hSheet.props.opened}
|
|
/>
|
|
</Sheet>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default MatchDock;
|