import { Form, Link, useFetcher } from "react-router"; import { Fragment, useState, useEffect, useMemo, useRef } from "react"; import { localDateTimeToUtcIso, utcIsoToLocalDateTime } from "~/lib/date-utils"; import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId.bracket"; import { loader, action } from "./admin.sports-seasons.$id.events.$eventId.bracket.server"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; import { ArrowLeft, Plus, Trophy, X, Check, ChevronDown, ChevronRight, Calendar, TrendingUp, Trash2 } from "lucide-react"; import { getAllBracketTemplates, getBracketTemplate, getOrderedRoundsFromMatches, buildNCAA68SlotMap, ALL_16_SEEDS, type BracketRegion } from "~/lib/bracket-templates"; import { computeGroupStandings } from "~/models/group-stage-match"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table"; import { Badge } from "~/components/ui/badge"; import { ParticipantSelector } from "~/components/ParticipantSelector"; export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { return [{ title: `Bracket — ${data?.event?.name ?? "Event"} - Brackt Admin` }]; } /** * Schedule editor for a single group-stage match. * * The admin enters a local wall-clock time; on submit it is converted to a UTC * ISO string (in the hidden `scheduledAt` field) so the DB stores a true UTC * instant. The stored UTC value is rendered back into the input in the * browser's local timezone via a client-only effect (avoids SSR hydration * mismatch, since the server runs in UTC). */ function GroupMatchScheduleForm({ match, localTzAbbr, }: { match: { id: string; scheduledAt: string | Date | null | undefined }; localTzAbbr: string; }) { const inputRef = useRef(null); useEffect(() => { if (inputRef.current) { inputRef.current.value = utcIsoToLocalDateTime(match.scheduledAt); } }, [match.scheduledAt]); return (
{ const form = e.currentTarget; const local = form.elements.namedItem("scheduledAtLocal") as HTMLInputElement; const hidden = form.elements.namedItem("scheduledAt") as HTMLInputElement; if (hidden) hidden.value = localDateTimeToUtcIso(local?.value) ?? ""; }} > {/* Hidden field holds the UTC ISO string written by onSubmit */} ); } export { loader, action }; /** * One "possible duplicate" row in the draw preview. Uses a fetcher so resolving * it (rename existing / create as new) submits in the background — the preview * stays on screen instead of reloading the page and forcing a fresh dry run. */ function DuplicateRow({ p, }: { p: { name: string; externalId: string | null; suggestion: string; suggestionId: string }; }) { const fetcher = useFetcher<{ success?: string; error?: string }>(); const busy = fetcher.state !== "idle"; return (
  • {p.name} → looks like {p.suggestion} {fetcher.data?.success ? ( ✓ {fetcher.data.success} ) : ( )} {fetcher.data?.error && ( {fetcher.data.error} )}
  • ); } export default function EventBracket({ loaderData, actionData, }: Route.ComponentProps) { const { sportsSeason, event, participants, matches, tournamentGroups } = loaderData; const [selectedTemplate, setSelectedTemplate] = useState("simple_8"); const [selectedParticipants, setSelectedParticipants] = useState>({}); const [selectedWinners, setSelectedWinners] = useState>({}); const [knockoutAssignments, setKnockoutAssignments] = useState>({}); const [expandedMatches, setExpandedMatches] = useState>(new Set()); const localTzAbbr = new Intl.DateTimeFormat("en-US", { timeZoneName: "short" }) .formatToParts(new Date()) .find(p => p.type === "timeZoneName")?.value ?? "local"; const toggleMatchExpanded = (matchId: string) => { setExpandedMatches(prev => { const next = new Set(prev); if (next.has(matchId)) next.delete(matchId); else next.add(matchId); return next; }); }; const templates = getAllBracketTemplates(); const template = templates.find((t) => t.id === selectedTemplate); // Determine if this is a group-stage event const hasGroupStage = template?.groupStage !== null && template?.groupStage !== undefined; // Determine if this template uses named regions (e.g. NCAA 68) const hasRegions = (template?.regions?.length ?? 0) > 0; // Per-region configuration state: name + which seed slots are play-ins // Initialized from: stored event config → template defaults → empty const buildDefaultRegionConfig = (tmpl: typeof template) => { const source = (event.bracketRegionConfig as unknown as BracketRegion[] | null) ?? tmpl?.regions ?? []; return source.map((r) => ({ name: r.name, playInSeeds: r.playIns.map((pi) => pi.seedSlot), })); }; const [regionConfig, setRegionConfig] = useState(() => buildDefaultRegionConfig(template)); // Derive effective BracketRegion[] from regionConfig state const effectiveRegions: BracketRegion[] = useMemo(() => { return regionConfig.map((cfg) => { const playInSet = new Set(cfg.playInSeeds); return { name: cfg.name, directSeeds: ALL_16_SEEDS.filter((s) => !playInSet.has(s)), playIns: cfg.playInSeeds.map((s) => ({ seedSlot: s, teams: 2 as const })), }; }); }, [regionConfig]); const regionSlotMap = useMemo( () => (effectiveRegions.length > 0 ? buildNCAA68SlotMap(effectiveRegions) : null), [effectiveRegions] ); // Region config handlers const updateRegionName = (regionIdx: number, name: string) => { setRegionConfig((prev) => prev.map((r, i) => (i === regionIdx ? { ...r, name } : r)) ); }; const addPlayIn = (regionIdx: number) => { setRegionConfig((prev) => prev.map((r, i) => { if (i !== regionIdx || r.playInSeeds.length >= 2) return r; // Default to seed 16 if no play-ins yet, otherwise seed 11 const defaultSeed = r.playInSeeds.length === 0 ? 16 : 11; const seed = r.playInSeeds.includes(defaultSeed) ? ([11, 16].find((s) => !r.playInSeeds.includes(s)) ?? 11) : defaultSeed; return { ...r, playInSeeds: [...r.playInSeeds, seed] }; }) ); // Reset participant selections since slot indices change setSelectedParticipants({}); }; const removePlayIn = (regionIdx: number, piIdx: number) => { setRegionConfig((prev) => prev.map((r, i) => i === regionIdx ? { ...r, playInSeeds: r.playInSeeds.filter((_, j) => j !== piIdx) } : r ) ); setSelectedParticipants({}); }; const updatePlayInSeed = (regionIdx: number, piIdx: number, seed: number) => { setRegionConfig((prev) => prev.map((r, i) => i === regionIdx ? { ...r, playInSeeds: r.playInSeeds.map((s, j) => (j === piIdx ? seed : s)), } : r ) ); setSelectedParticipants({}); }; const isGroupStageEvent = tournamentGroups.length > 0; // Determine the phase for group-stage events const knockoutPopulated = useMemo(() => { if (!isGroupStageEvent) return false; // Check if any Round of 32 match has participants assigned const r32Matches = matches.filter((m) => m.round === "Round of 32"); return r32Matches.some((m) => m.participant1Id || m.participant2Id); }, [isGroupStageEvent, matches]); // Group stage stats const groupStageStats = useMemo(() => { if (!isGroupStageEvent) return null; let eliminated = 0; let total = 0; for (const group of tournamentGroups) { for (const member of group.members || []) { total++; if (member.eliminated) eliminated++; } } return { eliminated, advancing: total - eliminated, total }; }, [isGroupStageEvent, tournamentGroups]); // Advancing participants (non-eliminated) for knockout assignment const advancingParticipants = useMemo(() => { if (!isGroupStageEvent) return []; const advancing: Array<{ id: string; name: string }> = []; for (const group of tournamentGroups) { for (const member of group.members || []) { if (!member.eliminated && member.participant) { advancing.push({ id: member.participant.id, name: member.participant.name, }); } } } return advancing; }, [isGroupStageEvent, tournamentGroups]); // Available advancing participants for knockout assignment (exclude already assigned) const availableAdvancingMap = useMemo(() => { const assignedIds = new Set(Object.values(knockoutAssignments).filter(Boolean)); const map: Record = {}; // Create 32 slots (16 matches × 2 slots) for (let m = 1; m <= 16; m++) { for (const slot of ["participant1Id", "participant2Id"]) { const key = `match-${m}-${slot}`; const currentSelection = knockoutAssignments[key]; map[key] = advancingParticipants.filter( (p) => !assignedIds.has(p.id) || p.id === currentSelection ); } } return map; }, [knockoutAssignments, advancingParticipants]); // Clear selected winners after successful batch submission useEffect(() => { if (actionData?.success) { setSelectedWinners({}); } }, [actionData?.success]); // Reset selected participants and region config when template changes const handleTemplateChange = (newTemplateId: string) => { setSelectedTemplate(newTemplateId); setSelectedParticipants({}); const newTemplate = templates.find((t) => t.id === newTemplateId); setRegionConfig(buildDefaultRegionConfig(newTemplate)); }; // Update selected participant for a seed const handleParticipantChange = (seedIndex: number, participantId: string) => { setSelectedParticipants(prev => ({ ...prev, [seedIndex]: participantId })); }; // Memoized available participants calculation const availableParticipantsMap = useMemo(() => { const selectedIdsSet = new Set(Object.values(selectedParticipants)); const map: Record = {}; const totalSeeds = template?.totalTeams || 8; for (let i = 0; i < totalSeeds; i++) { const currentSelection = selectedParticipants[i]; map[i] = participants.filter((p: { id: string; name: string }) => { return !selectedIdsSet.has(p.id) || p.id === currentSelection; }); } return map; }, [selectedParticipants, participants, template?.totalTeams]); const getAvailableParticipants = (seedIndex: number) => { return availableParticipantsMap[seedIndex] || participants; }; // Update selected winner for a match const handleWinnerChange = (matchId: string, winnerId: string) => { setSelectedWinners(prev => ({ ...prev, [matchId]: winnerId })); }; // Get matches with pending winner selections in a round const getPendingWinnersInRound = (round: string) => { return Object.entries(selectedWinners) .filter(([matchId]) => { const match = matches.find((m) => m.id === matchId); return match && match.round === round && !match.isComplete; }); }; // Memoized: Group matches by round const matchesByRound = useMemo(() => { const grouped: Record = {}; for (const match of matches) { if (!grouped[match.round]) { grouped[match.round] = []; } grouped[match.round].push(match); } return grouped; }, [matches]); // Memoized: Get available rounds in chronological order const availableRounds = useMemo(() => { const bracketTemplate = event.bracketTemplateId ? getBracketTemplate(event.bracketTemplateId) : undefined; return getOrderedRoundsFromMatches(matches, bracketTemplate); }, [matches, event.bracketTemplateId]); // Check if all matches are complete const allMatchesComplete = useMemo(() => { return matches.length > 0 && matches.every((m) => m.isComplete); }, [matches]); // No bracket and no groups yet = setup phase const showSetup = matches.length === 0 && !isGroupStageEvent; return (

    Playoff Bracket

    {event.name} - {sportsSeason.sport.name} {sportsSeason.name}

    {actionData?.error && (
    {actionData.error}
    )} {actionData?.success && (
    {actionData.success}
    )} {/* Possible-duplicate review after a draw sync */} {actionData?.drawSyncResult && actionData.drawSyncResult.unmatched.length > 0 && ( Review {actionData.drawSyncResult.unmatched.length} possible duplicate {actionData.drawSyncResult.unmatched.length === 1 ? "" : "s"} These players were auto-created but closely resemble an existing participant — if a duplicate, results won't reach the drafted copy. To fix: on the{" "} participants page , set the correct participant's external ID to the Wikipedia name, delete the duplicate, then click Sync Draw again.
      {actionData.drawSyncResult.unmatched.map((p) => (
    • {p.name} {p.externalId && p.externalId !== p.name && ( ({p.externalId}) )}
    • ))}
    )} {/* Dry-run preview of a draw sync */} {actionData?.drawPreview && ( Draw preview (no changes made) {actionData.drawPreview.article}
    Players: {actionData.drawPreview.totalPlayers}
    Matched:{" "} {actionData.drawPreview.matched}
    Will create:{" "} {actionData.drawPreview.willCreate.length}
    Matches:{" "} {actionData.drawPreview.completedMatches}/{actionData.drawPreview.totalMatches}{" "} done
    Unfilled R1 slots:{" "} {actionData.drawPreview.tbdFirstRound}
    {actionData.drawPreview.possibleDuplicates.length > 0 && (

    Possible duplicates ({actionData.drawPreview.possibleDuplicates.length}) — would be created but resemble an existing participant:

      {actionData.drawPreview.possibleDuplicates.map((p) => ( ))}

    Rename if it's the same player (links the existing participant so it matches);{" "} Create as new if they're different people. Then re-run Preview or Sync Draw.

    )} {actionData.drawPreview.willCreate.length > 0 && (
    Show all {actionData.drawPreview.willCreate.length} players that would be created
      {actionData.drawPreview.willCreate.map((p) => (
    • {p.name}
    • ))}
    )}

    Fix any duplicates on the{" "} participants page {" "} first, then click Sync Draw.

    )} {/* Sync Draw from Wikipedia — tennis Grand Slam auto-populate + auto-score */} {event.isQualifyingEvent && sportsSeason.sport.simulatorType === "tennis_qualifying_points" && ( Sync Draw from Wikipedia Auto-populate and score this Grand Slam bracket from its Wikipedia draw article. Re-run during the tournament to pull in completed matches.

    Paste the article URL or type the title — both work. Use{" "} Preview first to see matches and any new/duplicate players before writing anything.

    )} {/* Reprocess Bracket - Full rebuild of participant results. Hidden once every match is complete — at that point all placements are final and "finalize bracket" should be used instead. */} {matches.length > 0 && !matches.every((m) => m.isComplete) && ( Reprocess Bracket Replay all completed matches from scratch and re-mark non-bracket participants as eliminated. Use this to fix scoring data after rule changes or when results look incorrect.
    )} {/* ====== SETUP PHASE ====== */} {showSetup && ( Generate Bracket Create the bracket structure for this playoff event
    {/* Intent depends on whether this is a group-stage template */}
    {template && (

    {template.groupStage ? ( <> Groups: {template.groupStage.groupCount} groups of {template.groupStage.teamsPerGroup} {" | "}Knockout: {template.rounds.map(r => r.name).join(" \u2192 ")} ) : ( <>Rounds: {template.rounds.map(r => r.name).join(" \u2192 ")} )}

    )}
    {/* Group-stage template: show group assignments */} {hasGroupStage && template?.groupStage && (

    Assign {template.totalTeams} participants across {template.groupStage.groupCount} groups ({template.groupStage.teamsPerGroup} per group).

    {template.groupStage.groupLabels.map((label, groupIndex) => ( Group {label} {[...Array(template.groupStage?.teamsPerGroup ?? 0)].map((_, teamIndex) => { const seedIndex = groupIndex * (template.groupStage?.teamsPerGroup ?? 0) + teamIndex; const availableParticipants = getAvailableParticipants(seedIndex); return (
    handleParticipantChange(seedIndex, value)} placeholder={`Team ${teamIndex + 1}...`} />
    ); })}
    ))}
    )} {/* Non-group-stage template: region-grouped or flat seeded list */} {!hasGroupStage && hasRegions && regionSlotMap && (
    {/* ── Region configuration ─────────────────────────────── */}

    Configure Regions

    Set each region's name and which seed slots are play-in games.

    {regionConfig.map((region, r) => ( // eslint-disable-next-line react/no-array-index-key
    updateRegionName(r, e.target.value)} placeholder={`Region ${r + 1}`} /> {region.playInSeeds.map((seed, pi) => ( // eslint-disable-next-line react/no-array-index-key
    play-in seed:
    ))} {region.playInSeeds.length < 2 && ( )}
    ))} {/* Hidden fields so server receives the region config */} {regionConfig.map((region, r) => ( // eslint-disable-next-line react/no-array-index-key ))}
    {/* ── Seed entry per region ─────────────────────────────── */}

    Select participants by region. Use search to find teams quickly.

    {effectiveRegions.map((region, r) => { const directOffset = regionSlotMap.directOffsets[r]; return ( // eslint-disable-next-line react/no-array-index-key

    {region.name || `Region ${r + 1}`}

    {region.directSeeds.map((seedNum, pos) => { const globalIdx = directOffset + pos; const opponentSeed = 17 - seedNum; const opponentIsPlayIn = region.playIns.some((pi) => pi.seedSlot === opponentSeed); return (
    handleParticipantChange(globalIdx, value)} placeholder={`Seed ${seedNum}…`} />
    {opponentIsPlayIn && ( ← faces play-in )}
    ); })} {region.playIns.length > 0 && (

    {region.playIns.map((pi) => `Seed ${pi.seedSlot}`).join(" & ")} via play-in ↓

    )}
    ); })}
    {/* ── Play-in team entry ─────────────────────────────────── */} {regionSlotMap.playInOffsets.length > 0 && (

    Play-In Teams

    Enter both teams for each play-in game. The winner advances to the main bracket.

    {regionSlotMap.playInOffsets.map((pi, ffIdx) => { const regionName = effectiveRegions[pi.regionIndex]?.name || `Region ${pi.regionIndex + 1}`; const idx1 = pi.startIndex; const idx2 = pi.startIndex + 1; return (

    Play-In {ffIdx + 1} — {regionName} {pi.seedSlot}-seed

    {[idx1, idx2].map((globalIdx, slot) => (
    handleParticipantChange(globalIdx, value)} placeholder={`${regionName} ${pi.seedSlot}-seed play-in team ${slot + 1}…`} />
    ))}
    ); })}
    )}
    )} {/* Non-group-stage, non-region template: flat seeded list */} {!hasGroupStage && !hasRegions && (

    Select {template?.totalTeams || 8} participants for the bracket. Use search to quickly find participants.

    {[...Array(template?.totalTeams || 8)].map((_, i) => { const availableParticipants = getAvailableParticipants(i); const slotLabel = template?.participantLabels?.[i] ?? `Seed ${i + 1}`; return ( // eslint-disable-next-line react/no-array-index-key
    handleParticipantChange(i, value)} placeholder={`Select ${slotLabel.toLowerCase()}...`} />
    ); })}
    )}
    )} {/* ====== GROUP STAGE MANAGEMENT (Phase 2) ====== */} {isGroupStageEvent && !knockoutPopulated && ( <> {/* Group Stage Summary */} {groupStageStats && ( Group Stage Manage group stage eliminations. Mark teams as eliminated, then assign advancing teams to the knockout bracket.
    {groupStageStats.total} total teams {groupStageStats.eliminated} eliminated {groupStageStats.advancing} advancing
    )} {/* Group Cards */}
    {tournamentGroups.map((group) => { const members = group.members || []; const groupMatches = group.groupMatches || []; const standings = computeGroupStandings( members .filter((m) => m.participant !== null && m.participant !== undefined) .map((m) => ({ participantId: m.participant?.id ?? "", participantName: m.participant?.name ?? "", })), groupMatches.map((m) => ({ participant1Id: m.participant1Id, participant2Id: m.participant2Id, participant1Score: m.participant1Score, participant2Score: m.participant2Score, isComplete: m.isComplete, })) ); return ( Group {group.groupName} {/* Standings table */} {standings.length > 0 && (
    {standings.map((row, idx) => { const member = members.find((m) => m.participant?.id === row.participantId); const isTop2 = idx < 2; return ( ); })}
    Team P W D L GD Pts
    {row.participantName} {row.played} {row.wins} {row.draws} {row.losses} {row.gd > 0 ? `+${row.gd}` : row.gd} {row.points}
    )} {/* Elimination toggles */}
    {members.map((member) => (
    {member.participant?.name || "Unknown"}
    ))}
    {/* Match score entry */} {groupMatches.length > 0 && (

    Matches

    {groupMatches.map((match) => (
    {/* Schedule row */} {/* Score row */}
    {match.participant1?.name ?? "?"} {match.participant2?.name ?? "?"} {!match.isComplete && ( )} {match.isComplete && ( FT )}
    ))}
    )}
    ); })}
    {/* Knockout Assignment Section */} {groupStageStats && groupStageStats.advancing === 32 && ( Assign Knockout Bracket 32 teams are advancing. Assign them to the Round of 32 knockout bracket slots.
    {[...Array(16)].map((_, matchIndex) => { const matchNum = matchIndex + 1; const key1 = `match-${matchNum}-participant1Id`; const key2 = `match-${matchNum}-participant2Id`; return (
    Match {matchNum}
    setKnockoutAssignments((prev) => ({ ...prev, [key1]: value })) } placeholder="Team 1..." />
    vs
    setKnockoutAssignments((prev) => ({ ...prev, [key2]: value })) } placeholder="Team 2..." />
    ); })}
    )} {groupStageStats && groupStageStats.advancing !== 32 && groupStageStats.eliminated > 0 && (

    {groupStageStats.advancing} teams advancing (need exactly 32 to populate knockout bracket). {groupStageStats.advancing > 32 && ` Eliminate ${groupStageStats.advancing - 32} more.`} {groupStageStats.advancing < 32 && ` Reinstate ${32 - groupStageStats.advancing} teams or adjust eliminations.`}

    )} )} {/* ====== KNOCKOUT BRACKET DISPLAY (Phase 3) ====== */} {/* Show bracket rounds when: non-group event with matches, OR group event with knockout populated */} {((matches.length > 0 && !isGroupStageEvent) || knockoutPopulated) && ( <> {availableRounds.map((round: string) => ( {round} {matchesByRound[round].length} {matchesByRound[round].length === 1 ? "match" : "matches"} Match Participant 1 Score vs Score Participant 2 Winner Actions {matchesByRound[round].map((match) => ( {match.matchNumber} {match.participant1?.name || "TBD"} {match.participant1Score ? parseFloat(match.participant1Score) : "-"} vs {match.participant2Score ? parseFloat(match.participant2Score) : "-"} {match.participant2?.name || "TBD"} {match.winner?.name ? (
    {match.winner.name}
    ) : ( "-" )}
    {!match.isComplete && match.participant1Id && match.participant2Id ? ( ) : match.isComplete ? ( Complete ) : ( Waiting )}
    {/* Expanded: Games & Odds panel */} {expandedMatches.has(match.id) && (
    {/* Games section */}
    Game Schedule
    {/* Existing games */} {match.games?.length > 0 ? (
    {match.games.map((game) => (
    Game {game.gameNumber} {game.scheduledAt ? new Date(game.scheduledAt).toLocaleString() : "No date set"} {game.status} {game.status === "complete" && game.winner && ( W: {game.winner.name} )}
    ))}
    ) : (

    No games scheduled yet.

    )} {/* Add game form */}
    { const form = e.currentTarget; // Read from the datetime-local display input (not submitted directly) const scheduledAtLocal = form.elements.namedItem("scheduledAtLocal") as HTMLInputElement; // Write the UTC ISO string into the hidden input that IS submitted const scheduledAtHidden = form.elements.namedItem("scheduledAt") as HTMLInputElement; if (scheduledAtHidden) { scheduledAtHidden.value = localDateTimeToUtcIso(scheduledAtLocal?.value) ?? ""; } }}> {/* Hidden field holds the UTC ISO string written by onSubmit */}
    {/* Odds section */}
    Moneyline Odds
    {match.odds?.length > 0 ? (
    {match.odds.map((odd) => (
    {odd.participant?.name} 0 ? "text-emerald-500" : "text-muted-foreground"}`}> {odd.moneylineOdds > 0 ? "+" : ""}{odd.moneylineOdds} ({(parseFloat(odd.impliedProbability) * 100).toFixed(1)}%) {odd.oddsSource && ( {odd.oddsSource} )}
    ))}
    ) : (

    No odds set.

    )} {/* Set odds forms for each participant */} {[ { id: match.participant1Id, name: match.participant1?.name }, { id: match.participant2Id, name: match.participant2?.name }, ].filter(p => p.id).map(participant => (
    ))}
    )}
    ))}
    {/* Batch Submit Winners */} {getPendingWinnersInRound(round).length > 0 && (
    {getPendingWinnersInRound(round).map(([matchId, winnerId]) => ( ))}
    )}
    ))} {/* Finalize Bracket Button */} {allMatchesComplete && !event.isComplete && ( Finalize Bracket All matches are complete! Process all rounds and assign final placements.

    This will:

    • Process all playoff rounds in order
    • Assign fantasy placements (1st-8th) based on bracket results
    • Assign 0 points to participants not in the bracket
    • Mark the event as complete
    • Recalculate all team standings
    )} )} {/* Event Complete Badge */} {event.isComplete && (
    Event Complete {event.completedAt && new Date(event.completedAt).toLocaleDateString()}
    )}
    ); }