mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-30 15:17:43 -05:00
Streamline and fix tournament team export 0b download
This commit is contained in:
@@ -18,6 +18,11 @@
|
||||
var(--layout-nav-height) +
|
||||
env(safe-area-inset-bottom)
|
||||
);
|
||||
|
||||
/* snapshots paint above the top layer, which would lift it over an open modal's backdrop */
|
||||
:root:has(:modal) & {
|
||||
view-transition-name: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
z-index: 10;
|
||||
/* captured on its own so animating content slides under it instead of over it */
|
||||
view-transition-name: layout-header;
|
||||
|
||||
/* snapshots paint above the top layer, which would lift it over an open modal's backdrop */
|
||||
:root:has(:modal) & {
|
||||
view-transition-name: none;
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-group(layout-header) {
|
||||
|
||||
@@ -90,7 +90,7 @@ export function ExportDialog({
|
||||
|
||||
const [format, setFormat] = React.useState<ExportFormat>("list");
|
||||
const [status, setStatus] = React.useState<ExportStatus>("all");
|
||||
const [bracketIdx, setBracketIdx] = React.useState<number | null>(null);
|
||||
const [bracketIdx, setBracketIdx] = React.useState(0);
|
||||
const [sort, setSort] = React.useState<ExportSort>("seed");
|
||||
const [roster, setRoster] = React.useState<ExportRoster>("full");
|
||||
const [fields, setFields] = React.useState<Set<ExportField>>(
|
||||
@@ -108,35 +108,37 @@ export function ExportDialog({
|
||||
return next;
|
||||
});
|
||||
|
||||
const selectedBracket = tournament.bracketsMeta[bracketIdx];
|
||||
const bracketRequiresOwnCheckIn =
|
||||
selectedBracket.requiresCheckIn && !selectedBracket.isStartingBracket;
|
||||
const teams = scopedAndSortedTeams({
|
||||
teams: allTeams,
|
||||
status,
|
||||
sort,
|
||||
bracketIdx,
|
||||
bracketRequiresOwnCheckIn,
|
||||
bracketParticipantIds: new Set(
|
||||
tournament.eligibleTeamIdsOfBracket(bracketIdx),
|
||||
),
|
||||
});
|
||||
const content = buildContent({
|
||||
teams,
|
||||
format,
|
||||
fields,
|
||||
captainsOnly: roster === "captains",
|
||||
bracketIdx,
|
||||
bracketRequiresOwnCheckIn,
|
||||
checkedInLabel: "Checked in",
|
||||
notCheckedInLabel: "Not checked in",
|
||||
});
|
||||
const nothingToExportReason =
|
||||
teams.length === 0
|
||||
? "No teams match the selected bracket and status"
|
||||
: fields.size === 0 || !content
|
||||
? "Nothing to export with the selected fields"
|
||||
: null;
|
||||
|
||||
const onDownload = () => {
|
||||
const selectedBracket =
|
||||
bracketIdx !== null ? tournament.bracketsMeta[bracketIdx] : null;
|
||||
const bracketRequiresOwnCheckIn = Boolean(selectedBracket?.requiresCheckIn);
|
||||
const teams = scopedAndSortedTeams({
|
||||
teams: allTeams,
|
||||
status,
|
||||
sort,
|
||||
bracketIdx,
|
||||
bracketRequiresOwnCheckIn,
|
||||
// a team belongs to a bracket whether or not it has checked in (the rest are pending check-in);
|
||||
// both are scoped out of the export when they don't belong to it
|
||||
bracketParticipantIds: selectedBracket
|
||||
? new Set([
|
||||
...selectedBracket.participantTournamentTeamIds,
|
||||
...(selectedBracket.teamsPendingCheckIn ?? []),
|
||||
])
|
||||
: null,
|
||||
});
|
||||
const content = buildContent({
|
||||
teams,
|
||||
format,
|
||||
fields,
|
||||
captainsOnly: roster === "captains",
|
||||
bracketIdx,
|
||||
bracketRequiresOwnCheckIn,
|
||||
checkedInLabel: "Checked in",
|
||||
notCheckedInLabel: "Not checked in",
|
||||
});
|
||||
handleDownload({
|
||||
filename: `participants.${format === "csv" ? "csv" : "txt"}`,
|
||||
content,
|
||||
@@ -187,17 +189,12 @@ export function ExportDialog({
|
||||
{tournament.bracketsMeta.length > 1 ? (
|
||||
<RadioRow
|
||||
label="Bracket"
|
||||
value={bracketIdx === null ? "all" : String(bracketIdx)}
|
||||
onChange={(value) =>
|
||||
setBracketIdx(value === "all" ? null : Number(value))
|
||||
}
|
||||
options={[
|
||||
{ value: "all", label: "All brackets" },
|
||||
...tournament.bracketsMeta.map((bracket, idx) => ({
|
||||
value: String(idx),
|
||||
label: bracket.name || `#${idx}`,
|
||||
})),
|
||||
]}
|
||||
value={String(bracketIdx)}
|
||||
onChange={(value) => setBracketIdx(Number(value))}
|
||||
options={tournament.bracketsMeta.map((bracket, idx) => ({
|
||||
value: String(idx),
|
||||
label: bracket.name || `#${idx}`,
|
||||
}))}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -221,9 +218,17 @@ export function ExportDialog({
|
||||
}))}
|
||||
/>
|
||||
|
||||
<SendouButton onClick={onDownload} className="mx-auto">
|
||||
Download
|
||||
</SendouButton>
|
||||
<div className="stack sm items-center">
|
||||
<SendouButton
|
||||
onClick={onDownload}
|
||||
isDisabled={nothingToExportReason !== null}
|
||||
>
|
||||
Download
|
||||
</SendouButton>
|
||||
{nothingToExportReason ? (
|
||||
<div className="text-sm text-error">{nothingToExportReason}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</SendouDialog>
|
||||
);
|
||||
@@ -264,10 +269,10 @@ function RadioRow<T extends string>({
|
||||
|
||||
function hasActiveCheckIn(
|
||||
team: TournamentTeamFull,
|
||||
bracketIdx: number | null,
|
||||
bracketIdx: number,
|
||||
bracketRequiresOwnCheckIn: boolean,
|
||||
) {
|
||||
if (bracketIdx !== null && bracketRequiresOwnCheckIn) {
|
||||
if (bracketRequiresOwnCheckIn) {
|
||||
return team.checkIns.some(
|
||||
(checkIn) => checkIn.bracketIdx === bracketIdx && !checkIn.isCheckOut,
|
||||
);
|
||||
@@ -293,12 +298,12 @@ export function scopedAndSortedTeams({
|
||||
teams: TournamentTeamFull[];
|
||||
status: ExportStatus;
|
||||
sort: ExportSort;
|
||||
bracketIdx: number | null;
|
||||
bracketIdx: number;
|
||||
bracketRequiresOwnCheckIn: boolean;
|
||||
bracketParticipantIds: Set<number> | null;
|
||||
bracketParticipantIds: Set<number>;
|
||||
}) {
|
||||
const filtered = teams.filter((team) => {
|
||||
if (bracketParticipantIds && !bracketParticipantIds.has(team.id)) {
|
||||
if (!bracketParticipantIds.has(team.id)) {
|
||||
return false;
|
||||
}
|
||||
switch (status) {
|
||||
@@ -333,7 +338,7 @@ function teamFieldValue(
|
||||
opts: {
|
||||
checkedInLabel: string;
|
||||
notCheckedInLabel: string;
|
||||
bracketIdx: number | null;
|
||||
bracketIdx: number;
|
||||
bracketRequiresOwnCheckIn: boolean;
|
||||
},
|
||||
) {
|
||||
@@ -389,7 +394,7 @@ function buildContent({
|
||||
format: ExportFormat;
|
||||
fields: Set<ExportField>;
|
||||
captainsOnly: boolean;
|
||||
bracketIdx: number | null;
|
||||
bracketIdx: number;
|
||||
bracketRequiresOwnCheckIn: boolean;
|
||||
checkedInLabel: string;
|
||||
notCheckedInLabel: string;
|
||||
|
||||
@@ -449,6 +449,39 @@ describe("Adjusting team starting bracket", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("eligibleTeamIdsOfBracket", () => {
|
||||
const notCheckedIn = (teamId: number) =>
|
||||
tournamentCtxTeam(teamId, { checkIns: [] });
|
||||
|
||||
test("includes teams not checked in for the starting bracket", () => {
|
||||
const tournament = testTournament({
|
||||
ctx: {
|
||||
teams: [tournamentCtxTeam(1), tournamentCtxTeam(2), notCheckedIn(3)],
|
||||
},
|
||||
});
|
||||
|
||||
expect(tournament.eligibleTeamIdsOfBracket(0)).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test("includes only teams starting in the bracket when there are many starting brackets", () => {
|
||||
const tournament = testTournament({
|
||||
ctx: {
|
||||
teams: [
|
||||
tournamentCtxTeam(1, { startingBracketIdx: 0 }),
|
||||
tournamentCtxTeam(2, { startingBracketIdx: 1 }),
|
||||
notCheckedIn(3),
|
||||
],
|
||||
settings: {
|
||||
bracketProgression: progressions.manyStartBrackets,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(tournament.eligibleTeamIdsOfBracket(0)).toEqual([1, 3]);
|
||||
expect(tournament.eligibleTeamIdsOfBracket(1)).toEqual([2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("League divisions", () => {
|
||||
const leagueTournament = (isLeague = true) =>
|
||||
testTournament({
|
||||
|
||||
@@ -284,23 +284,28 @@ export class Tournament {
|
||||
}
|
||||
|
||||
/** Teams that can play in the bracket: its participants plus the ones still pending check-in. */
|
||||
eligibleTeamsCountOfBracket(bracketIdx: number) {
|
||||
eligibleTeamIdsOfBracket(bracketIdx: number) {
|
||||
const bracket = this.bracketsMeta[bracketIdx];
|
||||
|
||||
if (bracket.sources) {
|
||||
return (
|
||||
(bracket.teamsPendingCheckIn ?? []).length +
|
||||
bracket.participantTournamentTeamIds.length
|
||||
);
|
||||
return [
|
||||
...bracket.participantTournamentTeamIds,
|
||||
...(bracket.teamsPendingCheckIn ?? []),
|
||||
];
|
||||
}
|
||||
|
||||
if (!this.isMultiStartingBracket) {
|
||||
return this.ctx.teams.length;
|
||||
}
|
||||
const teams = this.isMultiStartingBracket
|
||||
? this.ctx.teams.filter(
|
||||
(team) => (team.startingBracketIdx ?? 0) === bracketIdx,
|
||||
)
|
||||
: this.ctx.teams;
|
||||
|
||||
return this.ctx.teams.filter(
|
||||
(team) => (team.startingBracketIdx ?? 0) === bracketIdx,
|
||||
).length;
|
||||
return teams.map((team) => team.id);
|
||||
}
|
||||
|
||||
/** Count of {@link eligibleTeamIdsOfBracket}. */
|
||||
eligibleTeamsCountOfBracket(bracketIdx: number) {
|
||||
return this.eligibleTeamIdsOfBracket(bracketIdx).length;
|
||||
}
|
||||
|
||||
/** Teams of the bracket: its participants, or every eligible team while it is a preview. */
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
navItem: medal
|
||||
type: bug
|
||||
---
|
||||
Participant export's "Not checked in" option now works for the starting bracket, the redundant "All brackets" option is removed, and downloading is disabled when there's nothing to export
|
||||
Reference in New Issue
Block a user