From e063f1a7fdab72195ea8f0dae18a7bebcb28e7ae Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sun, 28 Jun 2026 14:39:18 +0300 Subject: [PATCH] Add isCaptain filtering to tournament team export --- .../components/ExportDialog.tsx | 57 +++++++++++++++---- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/app/features/tournament-admin/components/ExportDialog.tsx b/app/features/tournament-admin/components/ExportDialog.tsx index 306de7bb7..3a43d4117 100644 --- a/app/features/tournament-admin/components/ExportDialog.tsx +++ b/app/features/tournament-admin/components/ExportDialog.tsx @@ -17,15 +17,21 @@ const BASE_URL = "https://sendou.ink"; const EXPORT_FORMATS = ["list", "csv"] as const; const EXPORT_STATUSES = ["all", "checkedIn", "notCheckedIn"] as const; const EXPORT_SORTS = ["name", "seed", "registration"] as const; +const EXPORT_ROSTERS = ["full", "captains"] as const; type ExportFormat = (typeof EXPORT_FORMATS)[number]; type ExportStatus = (typeof EXPORT_STATUSES)[number]; type ExportSort = (typeof EXPORT_SORTS)[number]; +type ExportRoster = (typeof EXPORT_ROSTERS)[number]; const FORMAT_LABELS: Record = { list: "List", csv: "CSV", }; +const ROSTER_LABELS: Record = { + full: "Full roster", + captains: "Captains only", +}; const STATUS_LABELS: Record = { all: "All teams", checkedIn: "Checked in only", @@ -80,6 +86,7 @@ export function ExportDialog({ close }: { close: () => void }) { const [status, setStatus] = React.useState("all"); const [bracketIdx, setBracketIdx] = React.useState(null); const [sort, setSort] = React.useState("seed"); + const [roster, setRoster] = React.useState("full"); const [fields, setFields] = React.useState>( new Set(DEFAULT_FIELDS), ); @@ -119,6 +126,7 @@ export function ExportDialog({ close }: { close: () => void }) { teams, format, fields, + captainsOnly: roster === "captains", bracketIdx, bracketRequiresOwnCheckIn, checkedInLabel: "Checked in", @@ -161,6 +169,16 @@ export function ExportDialog({ close }: { close: () => void }) { + ({ + value, + label: ROSTER_LABELS[value], + }))} + /> + {tournament.brackets.length > 1 ? ( ; + captainsOnly: boolean; bracketIdx: number | null; bracketRequiresOwnCheckIn: boolean; checkedInLabel: string; @@ -371,6 +391,10 @@ function buildContent({ }) { const teamFields = TEAM_FIELDS.filter((field) => fields.has(field)); const memberFields = MEMBER_FIELDS.filter((field) => fields.has(field)); + const membersOf = (team: TournamentDataTeam) => + captainsOnly + ? team.members.filter((member) => member.role === "OWNER") + : team.members; const labelOpts = { checkedInLabel, notCheckedInLabel, @@ -379,7 +403,10 @@ function buildContent({ }; if (format === "csv") { - const maxRoster = Math.max(0, ...teams.map((team) => team.members.length)); + const maxRoster = Math.max( + 0, + ...teams.map((team) => membersOf(team).length), + ); const header = [ ...teamFields.map((field) => FIELD_LABELS[field]), @@ -392,8 +419,9 @@ function buildContent({ const teamValues = teamFields.map((field) => teamFieldValue(team, field, labelOpts), ); + const teamMembers = membersOf(team); const memberValues = Array.from({ length: maxRoster }).flatMap((_, i) => { - const member = team.members[i]; + const member = teamMembers[i]; return memberFields.map((field) => member ? memberFieldValue(member, field) : "", ); @@ -405,8 +433,11 @@ function buildContent({ } // list: members grouped under each team (omitted when no member fields chosen, - // so a team-name-only export is just a plain list of names) + // so a team-name-only export is just a plain list of names). Members are only + // indented under a team header when team fields are present — without them the + // export is a tight flat list (e.g. captains-only + Discord mention). const hasMemberFields = memberFields.length > 0; + const groupByTeam = teamFields.length > 0; const entries = teams.map((team) => { const teamLine = teamFields @@ -414,17 +445,19 @@ function buildContent({ .filter(Boolean) .join(" - "); if (!hasMemberFields) return teamLine; - const memberLines = team.members.map( - (member) => - ` ${memberFields - .map((field) => memberFieldValue(member, field)) - .filter(Boolean) - .join(" - ")}`, - ); - return [teamLine, ...memberLines].join("\n"); + const memberLines = membersOf(team).map((member) => { + const memberLine = memberFields + .map((field) => memberFieldValue(member, field)) + .filter(Boolean) + .join(" - "); + return groupByTeam ? ` ${memberLine}` : memberLine; + }); + return [teamLine, ...memberLines].filter(Boolean).join("\n"); }); - return entries.join(hasMemberFields ? "\n\n" : "\n"); + return entries + .filter(Boolean) + .join(groupByTeam && hasMemberFields ? "\n\n" : "\n"); } function handleDownload({