Some checks failed
Deploy to Cloudflare Workers / deploy (push) Has been cancelled
243 lines
8.0 KiB
TypeScript
243 lines
8.0 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
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 { TeamIconPicker } from "@/components/teams/team-icon-picker";
|
|
import { TeamIcon } from "@/components/teams/TeamIcon";
|
|
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
|
import { Trash2 } from "lucide-react";
|
|
|
|
export type DraftTeam = {
|
|
id: string;
|
|
name: string;
|
|
nickname: string | null;
|
|
icon: string | null;
|
|
logo_path: string | null;
|
|
};
|
|
|
|
export function CompetitionDraftPanel({
|
|
competitionId,
|
|
initialTeams,
|
|
}: {
|
|
competitionId: string;
|
|
initialTeams: DraftTeam[];
|
|
}) {
|
|
const router = useRouter();
|
|
const [teams, setTeams] = useState<DraftTeam[]>(initialTeams);
|
|
const [name, setName] = useState("");
|
|
const [nickname, setNickname] = useState("");
|
|
const [icon, setIcon] = useState("shield");
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [confirmAction, setConfirmAction] = useState<
|
|
| { type: "activate" }
|
|
| { type: "fixtures" }
|
|
| { type: "delete-team"; team: DraftTeam }
|
|
| null
|
|
>(null);
|
|
|
|
const refreshTeams = useCallback(async () => {
|
|
const list = (await api.competitions.listTeams(competitionId)) as DraftTeam[];
|
|
setTeams(list);
|
|
}, [competitionId]);
|
|
|
|
async function run(fn: () => Promise<void>, refresh = true) {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
await fn();
|
|
if (refresh) {
|
|
await refreshTeams();
|
|
router.refresh();
|
|
}
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : "Error");
|
|
} finally {
|
|
setLoading(false);
|
|
setConfirmAction(null);
|
|
}
|
|
}
|
|
|
|
async function handleAddTeam(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
if (loading) return;
|
|
const trimmed = name.trim();
|
|
if (!trimmed) return;
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const created = (await api.competitions.createTeam(competitionId, {
|
|
name: trimmed,
|
|
nickname: nickname.trim() || undefined,
|
|
icon,
|
|
})) as DraftTeam;
|
|
setTeams((prev) => [...prev, created]);
|
|
setName("");
|
|
setNickname("");
|
|
setIcon("shield");
|
|
router.refresh();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Failed to add team");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{error && <p className="text-sm text-red-400">{error}</p>}
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button
|
|
variant="secondary"
|
|
disabled={loading}
|
|
onClick={() => setConfirmAction({ type: "activate" })}
|
|
>
|
|
Activate competition
|
|
</Button>
|
|
<Button
|
|
disabled={loading}
|
|
onClick={() => setConfirmAction({ type: "fixtures" })}
|
|
>
|
|
Generate fixtures
|
|
</Button>
|
|
</div>
|
|
|
|
<GlassCard title="Add team">
|
|
<form onSubmit={handleAddTeam} className="space-y-4">
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<Label htmlFor="team_name">Team name</Label>
|
|
<Input
|
|
id="team_name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="FC Example"
|
|
required
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="team_nickname">Nickname</Label>
|
|
<Input
|
|
id="team_nickname"
|
|
value={nickname}
|
|
onChange={(e) => setNickname(e.target.value)}
|
|
placeholder="The Blues"
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label className="mb-2 block">Team icon</Label>
|
|
<TeamIconPicker value={icon} onChange={setIcon} />
|
|
</div>
|
|
<Button type="submit" disabled={loading}>
|
|
{loading ? "Adding…" : "Add team"}
|
|
</Button>
|
|
</form>
|
|
</GlassCard>
|
|
|
|
<GlassCard title={`Teams (${teams.length})`}>
|
|
{teams.length === 0 ? (
|
|
<p className="text-sm text-[var(--color-muted)]">
|
|
No teams yet. Add your first team above.
|
|
</p>
|
|
) : (
|
|
<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">Team</th>
|
|
<th className="pb-3 pr-4">Nickname</th>
|
|
<th className="pb-3 w-16" />
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{teams.map((t) => (
|
|
<tr key={t.id} className="border-b border-white/5">
|
|
<td className="py-3 pr-4">
|
|
<div className="flex items-center gap-2 font-medium">
|
|
<TeamIcon
|
|
icon={t.icon}
|
|
className="h-5 w-5 text-cyan-400"
|
|
/>
|
|
{t.name}
|
|
</div>
|
|
</td>
|
|
<td className="py-3 pr-4 text-[var(--color-muted)]">
|
|
{t.nickname || "—"}
|
|
</td>
|
|
<td className="py-3 text-right">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
disabled={loading}
|
|
onClick={() =>
|
|
setConfirmAction({ type: "delete-team", team: t })
|
|
}
|
|
>
|
|
<Trash2 className="h-4 w-4 text-red-400" />
|
|
</Button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</GlassCard>
|
|
|
|
<ConfirmDialog
|
|
open={confirmAction?.type === "activate"}
|
|
onOpenChange={(o) => !o && setConfirmAction(null)}
|
|
title="Activate competition?"
|
|
description="This snapshots league rules and opens the season. Teams cannot be added casually after activation without master access."
|
|
confirmLabel="Activate"
|
|
loading={loading}
|
|
variant="primary"
|
|
onConfirm={() =>
|
|
run(() => api.competitions.activate(competitionId), true)
|
|
}
|
|
/>
|
|
|
|
<ConfirmDialog
|
|
open={confirmAction?.type === "fixtures"}
|
|
onOpenChange={(o) => !o && setConfirmAction(null)}
|
|
title="Generate fixtures?"
|
|
description="This creates all matches for every team. Make sure your team list is final."
|
|
confirmLabel="Generate"
|
|
loading={loading}
|
|
variant="primary"
|
|
onConfirm={() =>
|
|
run(() => api.competitions.generateFixtures(competitionId), true)
|
|
}
|
|
/>
|
|
|
|
<ConfirmDialog
|
|
open={confirmAction?.type === "delete-team"}
|
|
onOpenChange={(o) => !o && setConfirmAction(null)}
|
|
title="Remove team?"
|
|
description={`Delete "${confirmAction?.type === "delete-team" ? confirmAction.team.name : ""}" from this competition? This cannot be undone.`}
|
|
confirmLabel="Delete team"
|
|
loading={loading}
|
|
onConfirm={() => {
|
|
if (confirmAction?.type !== "delete-team") return;
|
|
const id = confirmAction.team.id;
|
|
return run(async () => {
|
|
await api.teams.delete(id);
|
|
setTeams((prev) => prev.filter((t) => t.id !== id));
|
|
}, true);
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|