Files
animethemes-web/src/components/bracket/BracketChart.tsx
T
Mani b0724e568a chore: Bumped most of the dependencies to the latest version (#229)
* Bumped Next.js to 15.
* Bumped React to 19.
* Bumped GraphQL to 16.
* Bumped styled-components to 6.
* Bumped Radix components to latest version.
* Migrated Framer Motion to new package.
* Migrated react-query to new package.
* Lots of TypeScript fixes.
* Removed FontAwesome Pro icons.
* Started fading out SWR.
* Dev Server now uses Turbopack.
2025-01-19 02:46:18 +01:00

238 lines
7.4 KiB
TypeScript

import { memo, useCallback, useRef, useState } from "react";
import type { RefObject } from "react";
import styled from "styled-components";
import { faDiagramProject } from "@fortawesome/free-solid-svg-icons";
import { m } from "motion/react";
import { BracketThemeSummaryCard } from "@/components/bracket/BracketThemeSummaryCard";
import { Button } from "@/components/button/Button";
import { Icon } from "@/components/icon/Icon";
import { Text } from "@/components/text/Text";
import type { BracketPageQuery } from "@/generated/graphql";
import theme from "@/theme";
import type { RequiredNonNullable } from "@/utils/types";
const StyledBracketContainer = styled.div`
flex: 1 1 0;
overflow: hidden;
user-select: none;
cursor: grab;
&:active {
cursor: grabbing;
}
&:fullscreen {
background-color: ${theme.colors["background"]};
}
`;
const StyledBracket = styled(m.div)`
display: flex;
position: relative;
width: min-content;
gap: 128px;
padding: 16px;
`;
const StyledCanvas = styled.canvas`
position: absolute;
inset: 0;
width: 100%;
height: 100%;
`;
const StyledRound = styled.div`
display: flex;
flex-direction: column;
gap: 64px;
`;
const StyledPairing = styled.div`
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 8px;
`;
const StyledBracketThemeSummaryCard = styled(BracketThemeSummaryCard)`
width: 384px;
`;
type BracketChartProps = RequiredNonNullable<BracketPageQuery>;
export function BracketChart({ bracket }: BracketChartProps) {
const [showBracketChart, setShowBracketChart] = useState(false);
const bracketRef = useRef<HTMLDivElement>(undefined);
const onBracketInit = useCallback((bracket: HTMLDivElement) => {
bracketRef.current = bracket;
if (bracket) {
bracket.requestFullscreen();
bracket.addEventListener("fullscreenchange", onFullscreenChange);
}
function onFullscreenChange() {
if (!document.fullscreenElement) {
setShowBracketChart(false);
bracket.removeEventListener("fullscreenchange", onFullscreenChange);
}
}
}, []);
const onCanvasInit = useCallback((canvas: HTMLCanvasElement) => {
if (!canvas?.parentNode) {
return;
}
const parentRect = (canvas.parentNode as HTMLDivElement).getBoundingClientRect();
canvas.width = parentRect.width;
canvas.height = parentRect.height;
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
ctx.strokeStyle = "#75ead4";
ctx.lineWidth = 2;
ctx.translate(-parentRect.left, -parentRect.top);
const cards = document.querySelectorAll<HTMLDivElement>("[data-theme]");
const cardsById = [...cards].reduce<Record<string, Array<HTMLDivElement>>>((map, card) => {
const id = card.dataset.theme;
if (id) {
if (!map[id]) {
map[id] = [];
}
map[id].push(card);
}
return map;
}, {});
for (const cards of Object.values(cardsById)) {
for (let i = 0; i < cards.length - 1; i++) {
const current = cards[i];
const currentRect = current.getBoundingClientRect();
const next = cards[i + 1];
const nextRect = next.getBoundingClientRect();
ctx.beginPath();
ctx.moveTo(currentRect.left + currentRect.width, currentRect.top + currentRect.height / 2);
ctx.bezierCurveTo(
nextRect.left - (nextRect.left - (currentRect.left + currentRect.width)) / 2,
currentRect.top + currentRect.height / 2,
nextRect.left - (nextRect.left - (currentRect.left + currentRect.width)) / 2,
nextRect.top + nextRect.height / 2,
nextRect.left,
nextRect.top + nextRect.height / 2,
);
ctx.stroke();
}
}
}, []);
return (
<>
<Button variant="primary" onClick={() => setShowBracketChart(true)} style={{ "--gap": "8px" }}>
<Icon icon={faDiagramProject} />
<span>Open Bracket Chart</span>
</Button>
{showBracketChart ? (
<StyledBracketContainer ref={onBracketInit}>
<StyledBracket drag dragConstraints={bracketRef as RefObject<HTMLDivElement | null>}>
<StyledCanvas ref={onCanvasInit} />
{bracket.rounds
.sort((a, b) => a.tier - b.tier)
.map((round) => (
<BracketRound key={round.tier} round={round} />
))}
</StyledBracket>
</StyledBracketContainer>
) : null}
</>
);
}
interface BracketRoundProps {
round: BracketChartProps["bracket"]["rounds"][number];
}
const BracketRound = memo(function BracketRound({ round }: BracketRoundProps) {
if (!round.pairings || !round.pairings.length) {
return null;
}
return (
<StyledRound>
<Text>{round.name}</Text>
<BracketPairings pairings={round.pairings} />
</StyledRound>
);
});
interface BracketPairingsProps {
pairings: BracketRoundProps["round"]["pairings"];
}
function BracketPairings({ pairings }: BracketPairingsProps) {
const sortedPairings = pairings
.sort((a, b) => a.group - b.group || a.order - b.order)
.map((pairing, index) => (
<StyledPairing key={index}>
<ContestantCard
key={pairing.characterA.id}
contestant={pairing.characterA}
opponent={pairing.characterB}
contestantVotes={pairing.votesA}
opponentVotes={pairing.votesB}
/>
<Text variant="small">VS</Text>
<ContestantCard
key={pairing.characterB.id}
contestant={pairing.characterB}
opponent={pairing.characterA}
contestantVotes={pairing.votesB}
opponentVotes={pairing.votesA}
/>
</StyledPairing>
));
return <>{sortedPairings}</>;
}
interface ContestantCardProps {
contestant:
| BracketPairingsProps["pairings"][number]["characterA"]
| BracketPairingsProps["pairings"][number]["characterB"];
opponent:
| BracketPairingsProps["pairings"][number]["characterA"]
| BracketPairingsProps["pairings"][number]["characterB"];
contestantVotes: number | null;
opponentVotes: number | null;
}
function ContestantCard({ contestant, opponent, contestantVotes, opponentVotes }: ContestantCardProps) {
const isVoted = !!contestantVotes && !!opponentVotes;
const isWinner =
isVoted &&
(contestantVotes !== opponentVotes ? contestantVotes > opponentVotes : contestant.seed < opponent.seed);
return (
<StyledBracketThemeSummaryCard
contestant={contestant}
isVoted={isVoted}
isWinner={isWinner}
seed={contestant.seed}
votes={contestantVotes}
data-theme={contestant.id}
/>
);
}