Some checks failed
Deploy to Cloudflare Workers / deploy (push) Has been cancelled
153 lines
5.3 KiB
TypeScript
153 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import Link from "next/link";
|
|
import { api } from "@/lib/api/client";
|
|
import { GlassCard } from "@/components/ui/glass-card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
|
import { PageHeader } from "@/components/dashboard/page-header";
|
|
import { Trash2, ExternalLink } from "lucide-react";
|
|
|
|
type League = {
|
|
id: string;
|
|
name: string;
|
|
description: string | null;
|
|
slug: string;
|
|
};
|
|
|
|
export function MasterLeaguesClient({ initialLeagues }: { initialLeagues: League[] }) {
|
|
const router = useRouter();
|
|
const [leagues, setLeagues] = useState(initialLeagues);
|
|
const [name, setName] = useState("");
|
|
const [description, setDescription] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
const [deleteTarget, setDeleteTarget] = useState<League | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
async function handleCreate(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
if (loading) return;
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const created = (await api.leagues.create({
|
|
name: name.trim(),
|
|
description: description.trim() || undefined,
|
|
})) as League;
|
|
setLeagues((prev) => [created, ...prev]);
|
|
setName("");
|
|
setDescription("");
|
|
router.refresh();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Failed to create");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<PageHeader
|
|
title="Leagues"
|
|
description="Create and manage tournament leagues"
|
|
/>
|
|
|
|
{error && <p className="text-sm text-red-400">{error}</p>}
|
|
|
|
<GlassCard title="New league">
|
|
<form onSubmit={handleCreate} className="flex flex-wrap items-end gap-4">
|
|
<div className="min-w-[200px] flex-1">
|
|
<Label>Name</Label>
|
|
<Input
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
required
|
|
className="mt-1"
|
|
placeholder="Sunday League"
|
|
/>
|
|
</div>
|
|
<div className="min-w-[200px] flex-1">
|
|
<Label>Description</Label>
|
|
<Input
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
<Button type="submit" disabled={loading}>
|
|
{loading ? "Creating…" : "Create league"}
|
|
</Button>
|
|
</form>
|
|
</GlassCard>
|
|
|
|
<GlassCard title={`All leagues (${leagues.length})`}>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-white/10 text-left text-[var(--color-muted)]">
|
|
<th className="pb-3 pr-4">Name</th>
|
|
<th className="pb-3 pr-4">ID</th>
|
|
<th className="pb-3 pr-4">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{leagues.map((l) => (
|
|
<tr key={l.id} className="border-b border-white/5">
|
|
<td className="py-3 pr-4 font-medium">{l.name}</td>
|
|
<td className="py-3 pr-4 font-mono text-xs text-[var(--color-muted)]">
|
|
{l.id.slice(0, 8)}…
|
|
</td>
|
|
<td className="py-3">
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" size="sm" asChild>
|
|
<Link href={`/master/leagues/${l.id}`}>
|
|
<ExternalLink className="h-3 w-3" />
|
|
Open
|
|
</Link>
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setDeleteTarget(l)}
|
|
>
|
|
<Trash2 className="h-4 w-4 text-red-400" />
|
|
</Button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</GlassCard>
|
|
|
|
<ConfirmDialog
|
|
open={!!deleteTarget}
|
|
onOpenChange={(o) => !o && setDeleteTarget(null)}
|
|
title="Delete league?"
|
|
description={`Permanently delete "${deleteTarget?.name}" and all competitions, teams, and matches? This cannot be undone.`}
|
|
confirmLabel="Delete league"
|
|
loading={loading}
|
|
onConfirm={async () => {
|
|
if (!deleteTarget) return;
|
|
setLoading(true);
|
|
try {
|
|
await api.leagues.delete(deleteTarget.id);
|
|
setLeagues((prev) => prev.filter((x) => x.id !== deleteTarget.id));
|
|
setDeleteTarget(null);
|
|
router.refresh();
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : "Delete failed");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|