2025-10-31 22:13:12 -07:00
|
|
|
import { Form, Link } from "react-router";
|
2026-03-15 10:22:42 -07:00
|
|
|
import { useState } from "react";
|
2025-10-31 22:13:12 -07:00
|
|
|
import type { Route } from "./+types/admin.sports-seasons.$id.events";
|
2026-03-10 12:10:52 -07:00
|
|
|
|
2025-10-31 22:13:12 -07:00
|
|
|
import { loader, action } from "./admin.sports-seasons.$id.events.server";
|
2026-03-15 10:22:42 -07:00
|
|
|
import { localDateTimeToUtcIso } from "~/lib/date-utils";
|
2025-10-31 22:13:12 -07:00
|
|
|
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 { Badge } from "~/components/ui/badge";
|
2026-03-07 21:59:29 -08:00
|
|
|
import { Textarea } from "~/components/ui/textarea";
|
|
|
|
|
import {
|
|
|
|
|
AlertDialog,
|
|
|
|
|
AlertDialogAction,
|
|
|
|
|
AlertDialogCancel,
|
|
|
|
|
AlertDialogContent,
|
|
|
|
|
AlertDialogDescription,
|
|
|
|
|
AlertDialogFooter,
|
|
|
|
|
AlertDialogHeader,
|
|
|
|
|
AlertDialogTitle,
|
|
|
|
|
AlertDialogTrigger,
|
|
|
|
|
} from "~/components/ui/alert-dialog";
|
|
|
|
|
import { Calendar, Trophy, ArrowLeft, Trash2, ListPlus } from "lucide-react";
|
|
|
|
|
import { format, parseISO } from "date-fns";
|
2025-11-11 10:08:25 -08:00
|
|
|
import { QualifyingPointsStandings } from "~/components/scoring/QualifyingPointsStandings";
|
2026-03-07 21:59:29 -08:00
|
|
|
import { getEventTypeLabel } from "~/models/scoring-event";
|
2025-10-31 22:13:12 -07:00
|
|
|
|
2026-03-10 12:10:52 -07:00
|
|
|
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
|
|
|
|
|
return [{ title: `Events — ${data?.sportsSeason?.name ?? "Sports Season"} - Brackt Admin` }];
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-31 22:13:12 -07:00
|
|
|
export { loader, action };
|
|
|
|
|
|
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* Fix no-shadow and consistent-function-scoping lint violations
Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint
warnings and promotes both rules to errors in .oxlintrc.json.
no-shadow: renamed Drizzle callback params (sports→s, matches→m,
seasons→s) to avoid shadowing outer imports; removed shadowed
destructures (eq, inArray) from where callbacks; renamed inner
template→bracketTemplate, prev→currentTimers, season→ss, name→teamName
(with name: teamName fix to preserve semantics).
consistent-function-scoping: moved formatDate, getRankBadge,
getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr,
elo (×2), weightedPick, sortByMatchNumber (×2) to module scope;
moved formatTime (×2), isValidLeagueName, getDraftTimes,
makeSeasonQueues to file scope in test files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix no-non-null-assertion lint violations and promote to error
Eliminates all 208 no-non-null-assertion warnings across 38 files.
Promotes typescript/no-non-null-assertion from warn to error in
.oxlintrc.json.
Fix patterns applied:
- Map.get(key)! after .has() check → extract with get() + null guard
- Map.get(key)! on pre-populated count maps → ?? 0 default
- .set(id, map.get(id)! + 1) increment → ?? 0 before adding
- participant1Id!/participant2Id! on DB matches → ?? "" fallback
- array.find()! in tests → guard + throw or expect().toBeDefined()
- bracketTemplateCache.get(id)! → null guard extract
- Various nullable field accesses → optional chain or ?? default
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers
Resolves all 9 remaining non-console lint warnings and promotes all
three rules to errors in .oxlintrc.json.
- prefer-add-event-listener: converted onchange/onclick/onload
assignments to addEventListener in useDraftNotifications.ts and
admin.data-sync.tsx; stored changeHandler ref for proper cleanup
with removeEventListener
- no-unassigned-import: configured rule with allow list for legitimate
side-effect imports (*.css, @testing-library/jest-dom,
@testing-library/cypress/add-commands)
- require-module-specifiers: removed redundant `export {}` from
cypress/support/e2e.ts (file already has an import)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix TypeScript errors from no-non-null-assertion fixes
Two fixes introduced by the non-null assertion cleanup produced type
errors:
- scoring-event.ts: `?? ""` was wrong type for a participant object map;
restructured to explicit null guards so TypeScript can narrow correctly
- standings-sync/index.ts: `?? null` after name-match lookup lost the
truthy guarantee, causing TS18047 on the write-back block; added
`participant &&` guard before accessing its properties
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add npm run typecheck as Stop hook in Claude settings
Runs a full project typecheck at the end of each Claude turn so type
errors surface as feedback before the next message.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00
|
|
|
function getStatusBadge(isComplete: boolean, eventType: string, eventDate?: string | null, eventStartsAt?: Date | string | null) {
|
|
|
|
|
if (isComplete) {
|
|
|
|
|
return (
|
|
|
|
|
<Badge variant="default" className="bg-emerald-500">
|
|
|
|
|
Completed
|
|
|
|
|
</Badge>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (eventType === "schedule_event") {
|
|
|
|
|
const isPast = eventStartsAt
|
|
|
|
|
? new Date(eventStartsAt) < new Date()
|
|
|
|
|
: eventDate !== null && eventDate !== undefined && eventDate < new Date().toISOString().split("T")[0];
|
|
|
|
|
return isPast ? (
|
|
|
|
|
<Badge variant="outline" className="text-muted-foreground">Results Pending</Badge>
|
|
|
|
|
) : (
|
|
|
|
|
<Badge variant="outline">Upcoming</Badge>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return <Badge variant="secondary">In Progress</Badge>;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-31 22:13:12 -07:00
|
|
|
export default function SportsSeasonEvents({
|
|
|
|
|
loaderData,
|
|
|
|
|
actionData,
|
|
|
|
|
}: Route.ComponentProps) {
|
2025-11-11 10:08:25 -08:00
|
|
|
const { sportsSeason, events, qpStandings, scoringRules } = loaderData;
|
2025-10-31 22:13:12 -07:00
|
|
|
|
2026-03-15 10:22:42 -07:00
|
|
|
const [createEventStartsAt, setCreateEventStartsAt] = useState("");
|
|
|
|
|
|
2026-03-07 21:59:29 -08:00
|
|
|
const defaultEventType =
|
|
|
|
|
sportsSeason.scoringPattern === "qualifying_points"
|
|
|
|
|
? "major_tournament"
|
|
|
|
|
: sportsSeason.scoringPattern === "season_standings"
|
|
|
|
|
? "schedule_event"
|
|
|
|
|
: "playoff_game";
|
2025-10-31 22:13:12 -07:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="p-8">
|
|
|
|
|
<div className="max-w-4xl">
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
<Button variant="ghost" size="sm" asChild className="mb-2">
|
|
|
|
|
<Link to={`/admin/sports-seasons/${sportsSeason.id}`}>
|
|
|
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
|
|
|
Back to Sports Season
|
|
|
|
|
</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
<h1 className="text-3xl font-bold">Scoring Events</h1>
|
|
|
|
|
<p className="text-muted-foreground mt-1">
|
|
|
|
|
{sportsSeason.sport.name} - {sportsSeason.name}
|
|
|
|
|
</p>
|
2025-11-11 10:08:25 -08:00
|
|
|
{sportsSeason.scoringPattern === "qualifying_points" && (
|
|
|
|
|
<div className="mt-2">
|
|
|
|
|
<Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-300 dark:bg-amber-950 dark:text-amber-300">
|
|
|
|
|
Qualifying Points Mode
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-10-31 22:13:12 -07:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-6">
|
2025-11-11 10:08:25 -08:00
|
|
|
{/* Success/Error Messages */}
|
|
|
|
|
{actionData?.error && (
|
|
|
|
|
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
|
|
|
|
|
{actionData.error}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{actionData?.success && (
|
2026-02-20 19:26:11 -08:00
|
|
|
<div className="bg-emerald-500/15 text-emerald-400 px-4 py-3 rounded-md text-sm">
|
2025-11-11 10:08:25 -08:00
|
|
|
{actionData.success}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* QP Standings for qualifying points sports */}
|
|
|
|
|
{sportsSeason.scoringPattern === "qualifying_points" && qpStandings && (
|
|
|
|
|
<QualifyingPointsStandings
|
|
|
|
|
standings={qpStandings}
|
|
|
|
|
scoringRules={scoringRules}
|
|
|
|
|
isFinalized={sportsSeason.qualifyingPointsFinalized || false}
|
|
|
|
|
totalMajors={sportsSeason.totalMajors}
|
|
|
|
|
majorsCompleted={sportsSeason.majorsCompleted}
|
|
|
|
|
canFinalize={
|
|
|
|
|
sportsSeason.totalMajors !== null &&
|
|
|
|
|
sportsSeason.majorsCompleted >= sportsSeason.totalMajors &&
|
|
|
|
|
!sportsSeason.qualifyingPointsFinalized
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-10-31 22:13:12 -07:00
|
|
|
{/* Create New Event Card */}
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>Create New Event</CardTitle>
|
|
|
|
|
<CardDescription>
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
Add a new scoring event (bracket, tournament, standings, etc.)
|
2025-10-31 22:13:12 -07:00
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<Form method="post" className="space-y-4">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="name">Event Name</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="name"
|
|
|
|
|
name="name"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="e.g., Super Bowl, Round 1, Game 3"
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-11-03 13:16:37 -08:00
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="eventType">Event Type</Label>
|
2025-11-11 10:08:25 -08:00
|
|
|
<Select
|
|
|
|
|
name="eventType"
|
2026-03-07 21:59:29 -08:00
|
|
|
defaultValue={defaultEventType}
|
2025-11-11 10:08:25 -08:00
|
|
|
required
|
|
|
|
|
>
|
2025-11-03 13:16:37 -08:00
|
|
|
<SelectTrigger id="eventType">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
2026-03-07 21:59:29 -08:00
|
|
|
<SelectItem value="schedule_event">Non-Scoring</SelectItem>
|
2025-11-03 13:16:37 -08:00
|
|
|
<SelectItem value="major_tournament">Major Tournament</SelectItem>
|
2026-03-07 21:59:29 -08:00
|
|
|
<SelectItem value="playoff_game">Bracket</SelectItem>
|
2025-11-03 13:16:37 -08:00
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2025-11-11 10:08:25 -08:00
|
|
|
{sportsSeason.scoringPattern === "qualifying_points" && (
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
Major Tournament events will automatically award qualifying points when completed.
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
2025-10-31 22:13:12 -07:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
2026-03-15 10:22:42 -07:00
|
|
|
<Label htmlFor="eventStartsAtLocal">Event Date & Time (Optional)</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="eventStartsAtLocal"
|
|
|
|
|
type="datetime-local"
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
setCreateEventStartsAt(localDateTimeToUtcIso(e.target.value) ?? "")
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<input type="hidden" name="eventStartsAt" value={createEventStartsAt} />
|
2025-10-31 22:13:12 -07:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{actionData?.error && (
|
|
|
|
|
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
|
|
|
|
|
{actionData.error}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<Button type="submit" className="w-full">
|
|
|
|
|
<Trophy className="mr-2 h-4 w-4" />
|
|
|
|
|
Create Event
|
|
|
|
|
</Button>
|
|
|
|
|
</Form>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
2026-03-07 21:59:29 -08:00
|
|
|
{/* Bulk Create Card */}
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="flex items-center gap-2">
|
|
|
|
|
<ListPlus className="h-5 w-5" />
|
|
|
|
|
Bulk Import Events
|
|
|
|
|
</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
Paste a schedule — one event per line. Format:{" "}
|
|
|
|
|
<code className="text-xs bg-muted px-1 rounded">
|
2026-03-15 10:22:42 -07:00
|
|
|
Event Name, YYYY-MM-DD[, HH:MM]
|
2026-03-07 21:59:29 -08:00
|
|
|
</code>{" "}
|
2026-03-15 10:22:42 -07:00
|
|
|
(date and time are optional; time is UTC).
|
2026-03-07 21:59:29 -08:00
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<Form method="post" className="space-y-4">
|
|
|
|
|
<input type="hidden" name="intent" value="bulk-create" />
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="bulkEventType">Event Type</Label>
|
|
|
|
|
<Select name="bulkEventType" defaultValue={defaultEventType}>
|
|
|
|
|
<SelectTrigger id="bulkEventType">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="schedule_event">Non-Scoring</SelectItem>
|
|
|
|
|
<SelectItem value="major_tournament">Major Tournament</SelectItem>
|
|
|
|
|
<SelectItem value="playoff_game">Bracket</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="bulkEvents">Events</Label>
|
|
|
|
|
<Textarea
|
|
|
|
|
id="bulkEvents"
|
|
|
|
|
name="bulkEvents"
|
|
|
|
|
rows={6}
|
2026-03-15 10:22:42 -07:00
|
|
|
placeholder={"Bahrain Grand Prix, 2025-03-02, 15:00\nSaudi Arabian Grand Prix, 2025-03-09, 18:00\nAustralian Grand Prix, 2025-03-23"}
|
2026-03-07 21:59:29 -08:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<Button type="submit" variant="outline" className="w-full">
|
|
|
|
|
<ListPlus className="mr-2 h-4 w-4" />
|
|
|
|
|
Import Events
|
|
|
|
|
</Button>
|
|
|
|
|
</Form>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
2025-10-31 22:13:12 -07:00
|
|
|
{/* Events List */}
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>Events</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
{events.length} {events.length === 1 ? "event" : "events"}
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{events.length === 0 ? (
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
No events created yet. Create your first event above.
|
|
|
|
|
</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-3">
|
2026-03-15 10:22:42 -07:00
|
|
|
{events.map((event: { id: string; name: string; eventType: string; eventDate?: string | null; eventStartsAt?: Date | string | null; isComplete: boolean }) => (
|
2025-10-31 22:13:12 -07:00
|
|
|
<Card key={event.id} className="hover:border-primary/50 transition-colors">
|
|
|
|
|
<CardContent className="pt-6">
|
|
|
|
|
<div className="flex items-start justify-between gap-4">
|
|
|
|
|
<Link
|
|
|
|
|
to={`/admin/sports-seasons/${sportsSeason.id}/events/${event.id}`}
|
|
|
|
|
className="flex-1 cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2 mb-1">
|
|
|
|
|
<h3 className="font-semibold">{event.name}</h3>
|
2026-03-15 12:28:39 -07:00
|
|
|
{getStatusBadge(event.isComplete, event.eventType, event.eventDate, event.eventStartsAt)}
|
2025-10-31 22:13:12 -07:00
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
|
|
|
|
<span>
|
|
|
|
|
{getEventTypeLabel(event.eventType)}
|
|
|
|
|
</span>
|
2026-03-15 11:18:16 -07:00
|
|
|
{(event.eventDate || event.eventStartsAt) && (
|
2025-10-31 22:13:12 -07:00
|
|
|
<span className="flex items-center gap-1">
|
|
|
|
|
<Calendar className="h-3 w-3" />
|
2026-03-15 11:18:16 -07:00
|
|
|
<span suppressHydrationWarning>
|
|
|
|
|
{format(
|
|
|
|
|
event.eventStartsAt
|
|
|
|
|
? new Date(event.eventStartsAt)
|
|
|
|
|
: parseISO(event.eventDate ?? ""),
|
|
|
|
|
"MMM d, yyyy"
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
2026-03-15 10:22:42 -07:00
|
|
|
{event.eventStartsAt && (
|
|
|
|
|
<span className="text-muted-foreground/70" suppressHydrationWarning>
|
|
|
|
|
· {format(new Date(event.eventStartsAt), "h:mm a")}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2025-10-31 22:13:12 -07:00
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</Link>
|
2026-03-07 21:59:29 -08:00
|
|
|
<AlertDialog>
|
|
|
|
|
<AlertDialogTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
className="text-destructive hover:text-destructive shrink-0"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</AlertDialogTrigger>
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>Delete "{event.name}"?</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
This will also delete all results for this event. This action cannot be undone.
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
|
|
|
<Form method="post">
|
|
|
|
|
<input type="hidden" name="intent" value="delete-event" />
|
|
|
|
|
<input type="hidden" name="eventId" value={event.id} />
|
|
|
|
|
<AlertDialogAction type="submit" className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
|
|
|
|
Delete
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
</Form>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
2025-10-31 22:13:12 -07:00
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|