mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-01 15:48:23 -05:00
Move app to apps/web-react in pnpm workspace layout
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
.settings {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--s-4);
|
||||
align-items: flex-end;
|
||||
padding: var(--s-4);
|
||||
background: var(--color-bg-high);
|
||||
border-radius: var(--rounded);
|
||||
margin-bottom: var(--s-6);
|
||||
}
|
||||
|
||||
.settingGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.teamCountInput {
|
||||
width: 5rem;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { useOutletContext } from "react-router";
|
||||
import { Bracket } from "~/features/tournament-bracket/components/Bracket";
|
||||
import type { Bracket as BracketType } from "~/features/tournament-bracket/core/Bracket";
|
||||
|
||||
export default function BracketTestIndex() {
|
||||
const { bracket } = useOutletContext<{ bracket: BracketType }>();
|
||||
|
||||
return <Bracket bracket={bracket} bracketIdx={0} />;
|
||||
}
|
||||
306
apps/web-react/app/features/bracket-test/routes/bracket-test.tsx
Normal file
306
apps/web-react/app/features/bracket-test/routes/bracket-test.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
import { useState } from "react";
|
||||
import { Outlet } from "react-router";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import { Input } from "~/components/Input";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Main } from "~/components/Main";
|
||||
import type { Tables } from "~/db/tables";
|
||||
import { TournamentProvider } from "~/features/tournament/tournament-context";
|
||||
import type { Bracket as BracketType } from "~/features/tournament-bracket/core/Bracket";
|
||||
import * as Engine from "~/features/tournament-bracket/core/engine";
|
||||
import type { BracketData } from "~/features/tournament-bracket/core/engine/types";
|
||||
import type { Tournament as TournamentClass } from "~/features/tournament-bracket/core/Tournament";
|
||||
import styles from "../bracket-test.module.css";
|
||||
|
||||
type FormatType = Tables["TournamentStage"]["type"];
|
||||
|
||||
const FORMAT_OPTIONS: { value: FormatType; label: string }[] = [
|
||||
{ value: "single_elimination", label: "Single Elim" },
|
||||
{ value: "double_elimination", label: "Double Elim" },
|
||||
{ value: "round_robin", label: "Round Robin" },
|
||||
{ value: "swiss", label: "Swiss" },
|
||||
];
|
||||
|
||||
export default function BracketTestLayout() {
|
||||
const [format, setFormat] = useState<FormatType>("double_elimination");
|
||||
const [teamCount, setTeamCount] = useState(8);
|
||||
const [bracketExpanded, setBracketExpanded] = useState(true);
|
||||
const [completedRounds, setCompletedRounds] = useState(0);
|
||||
const [completedWbRounds, setCompletedWbRounds] = useState(0);
|
||||
const [completedLbRounds, setCompletedLbRounds] = useState(0);
|
||||
|
||||
const clampedTeamCount = Math.max(2, teamCount);
|
||||
const teams = generateTeams(clampedTeamCount);
|
||||
const teamIds = teams.map((t) => t.id);
|
||||
|
||||
const data = generateBracketData(format, teamIds);
|
||||
const isDoubleElim = format === "double_elimination";
|
||||
const { totalRounds, wbRounds, lbRounds } = countRounds(data, isDoubleElim);
|
||||
|
||||
if (isDoubleElim) {
|
||||
simulateCompletedRoundsByGroup(data, completedWbRounds, completedLbRounds);
|
||||
} else {
|
||||
simulateCompletedRounds(data, Math.min(completedRounds, totalRounds));
|
||||
}
|
||||
|
||||
const mockTournament = {
|
||||
ctx: {
|
||||
id: 1,
|
||||
name: "Bracket Test",
|
||||
isFinalized: 0,
|
||||
castedMatchesInfo: null,
|
||||
teams,
|
||||
settings: {
|
||||
bracketProgression: [
|
||||
{
|
||||
name: "Test Bracket",
|
||||
type: format,
|
||||
requiresCheckIn: false,
|
||||
settings: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
bracketProgressionOverrides: [],
|
||||
},
|
||||
participatedUserIds: teamIds,
|
||||
brackets: [] as unknown[],
|
||||
bracketsMeta: [] as unknown[],
|
||||
bracketMetaByIdx: () => null,
|
||||
teamById: (id: number) => teams.find((t) => t.id === id) ?? null,
|
||||
teamMemberOfByUser: () => null,
|
||||
isOrganizer: () => false,
|
||||
streamingParticipantIds: [] as number[],
|
||||
streams: [] as unknown[],
|
||||
isLeagueDivision: false,
|
||||
};
|
||||
|
||||
const mockBracket = {
|
||||
id: 1,
|
||||
idx: 0,
|
||||
preview: false,
|
||||
data,
|
||||
type: format,
|
||||
name: "Test Bracket",
|
||||
canBeStarted: false,
|
||||
tournament: mockTournament,
|
||||
settings: format === "swiss" ? { roundCount: 5 } : null,
|
||||
sources: undefined,
|
||||
seeding: undefined,
|
||||
createdAt: null,
|
||||
requiresCheckIn: false,
|
||||
startTime: null,
|
||||
simulatedMatch: () => undefined,
|
||||
liveStandings: [],
|
||||
participantTournamentTeamIds: teamIds,
|
||||
everyMatchOver: false,
|
||||
isUnderground: false,
|
||||
} as unknown as BracketType;
|
||||
|
||||
mockTournament.brackets = [mockBracket];
|
||||
|
||||
return (
|
||||
<Main bigger>
|
||||
<h1 className="text-lg">Bracket Test</h1>
|
||||
<div className={styles.settings}>
|
||||
<div className={styles.settingGroup}>
|
||||
<Label>Format</Label>
|
||||
<div className="stack horizontal sm">
|
||||
{FORMAT_OPTIONS.map((opt) => (
|
||||
<SendouButton
|
||||
key={opt.value}
|
||||
variant={format === opt.value ? undefined : "outlined"}
|
||||
size="small"
|
||||
onPress={() => setFormat(opt.value)}
|
||||
>
|
||||
{opt.label}
|
||||
</SendouButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.settingGroup}>
|
||||
<Label htmlFor="team-count">Teams</Label>
|
||||
<Input
|
||||
id="team-count"
|
||||
type="number"
|
||||
min={2}
|
||||
max={128}
|
||||
value={String(teamCount)}
|
||||
onChange={(e) => setTeamCount(Number(e.target.value))}
|
||||
className={styles.teamCountInput}
|
||||
/>
|
||||
</div>
|
||||
{isDoubleElim ? (
|
||||
<>
|
||||
<div className={styles.settingGroup}>
|
||||
<Label htmlFor="completed-wb">Completed WB rounds</Label>
|
||||
<Input
|
||||
id="completed-wb"
|
||||
type="number"
|
||||
min={0}
|
||||
max={wbRounds}
|
||||
value={String(Math.min(completedWbRounds, wbRounds))}
|
||||
onChange={(e) => setCompletedWbRounds(Number(e.target.value))}
|
||||
className={styles.teamCountInput}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.settingGroup}>
|
||||
<Label htmlFor="completed-lb">Completed LB rounds</Label>
|
||||
<Input
|
||||
id="completed-lb"
|
||||
type="number"
|
||||
min={0}
|
||||
max={lbRounds}
|
||||
value={String(Math.min(completedLbRounds, lbRounds))}
|
||||
onChange={(e) => setCompletedLbRounds(Number(e.target.value))}
|
||||
className={styles.teamCountInput}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.settingGroup}>
|
||||
<Label htmlFor="completed-rounds">Completed rounds</Label>
|
||||
<Input
|
||||
id="completed-rounds"
|
||||
type="number"
|
||||
min={0}
|
||||
max={totalRounds}
|
||||
value={String(Math.min(completedRounds, totalRounds))}
|
||||
onChange={(e) => setCompletedRounds(Number(e.target.value))}
|
||||
className={styles.teamCountInput}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.settingGroup}>
|
||||
<SendouSwitch
|
||||
id="expanded"
|
||||
isSelected={bracketExpanded}
|
||||
onChange={setBracketExpanded}
|
||||
>
|
||||
Expanded
|
||||
</SendouSwitch>
|
||||
</div>
|
||||
</div>
|
||||
<TournamentProvider
|
||||
tournament={mockTournament as unknown as TournamentClass}
|
||||
>
|
||||
<Outlet
|
||||
context={{
|
||||
tournament: mockTournament,
|
||||
bracketExpanded,
|
||||
setBracketExpanded,
|
||||
hasChildTournaments: false,
|
||||
preparedMaps: null,
|
||||
bracket: mockBracket,
|
||||
}}
|
||||
/>
|
||||
</TournamentProvider>
|
||||
</Main>
|
||||
);
|
||||
}
|
||||
|
||||
function generateTeams(count: number) {
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: `Team ${i + 1}`,
|
||||
seed: i + 1,
|
||||
members: [{ userId: i + 1, username: `Player${i + 1}` }],
|
||||
droppedOut: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
function countRounds(data: BracketData, isDoubleElim: boolean) {
|
||||
const totalRounds = Math.max(...data.round.map((r) => r.number));
|
||||
|
||||
if (!isDoubleElim) return { totalRounds, wbRounds: 0, lbRounds: 0 };
|
||||
|
||||
const wbGroupId = data.group.find((g) => g.number === 1)?.id;
|
||||
const lbGroupId = data.group.find((g) => g.number === 2)?.id;
|
||||
|
||||
const wbRounds = data.round.filter((r) => r.groupId === wbGroupId).length;
|
||||
const lbRounds = data.round.filter((r) => r.groupId === lbGroupId).length;
|
||||
|
||||
return { totalRounds, wbRounds, lbRounds };
|
||||
}
|
||||
|
||||
function simulateCompletedRoundsByGroup(
|
||||
data: BracketData,
|
||||
wbCompleted: number,
|
||||
lbCompleted: number,
|
||||
) {
|
||||
const wbGroupId = data.group.find((g) => g.number === 1)?.id;
|
||||
const lbGroupId = data.group.find((g) => g.number === 2)?.id;
|
||||
|
||||
const completedRoundIds = new Set<number>();
|
||||
for (const round of data.round) {
|
||||
if (round.groupId === wbGroupId && round.number <= wbCompleted) {
|
||||
completedRoundIds.add(round.id);
|
||||
}
|
||||
if (round.groupId === lbGroupId && round.number <= lbCompleted) {
|
||||
completedRoundIds.add(round.id);
|
||||
}
|
||||
}
|
||||
|
||||
markMatchesCompleted(data, completedRoundIds);
|
||||
}
|
||||
|
||||
function simulateCompletedRounds(data: BracketData, completedRounds: number) {
|
||||
if (completedRounds <= 0) return;
|
||||
|
||||
const roundsByNumber = new Map<number, number[]>();
|
||||
for (const round of data.round) {
|
||||
const existing = roundsByNumber.get(round.number) ?? [];
|
||||
existing.push(round.id);
|
||||
roundsByNumber.set(round.number, existing);
|
||||
}
|
||||
|
||||
const completedRoundIds = new Set<number>();
|
||||
for (let n = 1; n <= completedRounds; n++) {
|
||||
for (const id of roundsByNumber.get(n) ?? []) {
|
||||
completedRoundIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
markMatchesCompleted(data, completedRoundIds);
|
||||
}
|
||||
|
||||
function markMatchesCompleted(
|
||||
data: BracketData,
|
||||
completedRoundIds: Set<number>,
|
||||
) {
|
||||
for (const match of data.match) {
|
||||
if (!completedRoundIds.has(match.roundId)) continue;
|
||||
// skip BYE matches (opponent slot is null entirely)
|
||||
if (match.opponent1 === null || match.opponent2 === null) continue;
|
||||
|
||||
match.opponent1 = { ...match.opponent1, score: 2 };
|
||||
match.opponent2 = { ...match.opponent2, score: 0 };
|
||||
match.winnerSide = "opponent1";
|
||||
}
|
||||
}
|
||||
|
||||
function generateBracketData(
|
||||
format: FormatType,
|
||||
teamIds: number[],
|
||||
): BracketData {
|
||||
if (format === "swiss") {
|
||||
return Engine.create({
|
||||
type: "swiss",
|
||||
seeding: teamIds,
|
||||
settings: { groupCount: 1, roundCount: 5 },
|
||||
});
|
||||
}
|
||||
|
||||
const settings =
|
||||
format === "single_elimination"
|
||||
? { thirdPlaceMatch: false }
|
||||
: format === "double_elimination"
|
||||
? null
|
||||
: { teamsPerGroup: 4 };
|
||||
|
||||
return Engine.create({
|
||||
type: format,
|
||||
seeding: teamIds,
|
||||
settings,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user