259 lines
8.2 KiB
TypeScript
259 lines
8.2 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { Trash2, Plus, Pencil, X } from "lucide-react";
|
|
import Shell from "@/components/Shell";
|
|
import Badge, { type BadgeTone } from "@/components/Badge";
|
|
import Empty from "@/components/Empty";
|
|
import { TableSkeleton } from "@/components/Skeleton";
|
|
import { api } from "@/lib/api";
|
|
import { toastError } from "@/lib/toast";
|
|
|
|
type Tournament = {
|
|
id: number;
|
|
title: string;
|
|
description: string;
|
|
entry_fee: number;
|
|
prizes: number[];
|
|
prize_pool: number;
|
|
status: string;
|
|
players: number;
|
|
starts_local: string;
|
|
ends_local: string;
|
|
};
|
|
|
|
const STATUS: Record<string, { t: string; tone: BadgeTone }> = {
|
|
active: { t: "در حال برگزاری", tone: "green" },
|
|
upcoming: { t: "بهزودی", tone: "gold" },
|
|
ended: { t: "پایانیافته", tone: "gray" },
|
|
};
|
|
|
|
const EMPTY = {
|
|
title: "",
|
|
description: "",
|
|
entry_fee: 0,
|
|
prizes: "",
|
|
starts_at: "",
|
|
ends_at: "",
|
|
};
|
|
|
|
// "2026-07-07 13:00" → "2026-07-07T13:00" (ورودیِ datetime-local)
|
|
const toLocalInput = (s: string) => (s || "").replace(" ", "T");
|
|
|
|
export default function TournamentsPage() {
|
|
const [list, setList] = useState<Tournament[]>([]);
|
|
const [form, setForm] = useState({ ...EMPTY });
|
|
const [editingId, setEditingId] = useState<number | null>(null);
|
|
const [busy, setBusy] = useState(false);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
async function load() {
|
|
setLoading(true);
|
|
try {
|
|
const r = await api.get<{ tournaments: Tournament[] }>("/tournaments");
|
|
setList(r.tournaments || []);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
useEffect(() => {
|
|
load();
|
|
}, []);
|
|
|
|
function startEdit(t: Tournament) {
|
|
setEditingId(t.id);
|
|
setForm({
|
|
title: t.title,
|
|
description: t.description || "",
|
|
entry_fee: t.entry_fee,
|
|
prizes: (t.prizes || []).join(","),
|
|
starts_at: toLocalInput(t.starts_local),
|
|
ends_at: toLocalInput(t.ends_local),
|
|
});
|
|
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
|
|
}
|
|
|
|
function cancelEdit() {
|
|
setEditingId(null);
|
|
setForm({ ...EMPTY });
|
|
}
|
|
|
|
async function submit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setBusy(true);
|
|
try {
|
|
const payload = {
|
|
title: form.title,
|
|
description: form.description,
|
|
entry_fee: Number(form.entry_fee),
|
|
prizes: form.prizes
|
|
.split(",")
|
|
.map((s) => parseInt(s.trim(), 10))
|
|
.filter((n) => !isNaN(n)),
|
|
starts_at: form.starts_at,
|
|
ends_at: form.ends_at,
|
|
};
|
|
if (editingId) {
|
|
await api.put("/tournaments", { id: editingId, ...payload });
|
|
} else {
|
|
await api.post("/tournaments", payload);
|
|
}
|
|
cancelEdit();
|
|
await load();
|
|
} catch (e) {
|
|
toastError((e as Error).message);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
async function remove(id: number) {
|
|
if (!confirm("حذف تورنومنت؟")) return;
|
|
await api.del(`/tournaments?id=${id}`);
|
|
if (editingId === id) cancelEdit();
|
|
await load();
|
|
}
|
|
|
|
return (
|
|
<Shell title="تورنومنتها">
|
|
{loading ? (
|
|
<TableSkeleton rows={5} cols={9} />
|
|
) : list.length === 0 ? (
|
|
<Empty
|
|
title="تورنومنتی وجود ندارد"
|
|
hint="با فرمِ پایینِ صفحه، اولین تورنومنت را بسازید."
|
|
/>
|
|
) : (
|
|
<div className="card overflow-x-auto mb-6">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>#</th>
|
|
<th>عنوان</th>
|
|
<th>ورودی</th>
|
|
<th>جوایز</th>
|
|
<th>وضعیت</th>
|
|
<th>بازیکنان</th>
|
|
<th>شروع (تهران)</th>
|
|
<th>پایان (تهران)</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{list.map((t) => {
|
|
const s = STATUS[t.status] || STATUS.ended;
|
|
return (
|
|
<tr key={t.id} className={editingId === t.id ? "bg-[rgba(233,185,73,.08)]" : ""}>
|
|
<td className="text-[#8aa]">{t.id}</td>
|
|
<td className="font-bold">{t.title}</td>
|
|
<td>{t.entry_fee}</td>
|
|
<td className="text-[var(--gold)]">{(t.prizes || []).join("، ")}</td>
|
|
<td>
|
|
<Badge tone={s.tone}>{s.t}</Badge>
|
|
</td>
|
|
<td>{t.players}</td>
|
|
<td dir="ltr" className="text-xs">{t.starts_local}</td>
|
|
<td dir="ltr" className="text-xs">{t.ends_local}</td>
|
|
<td className="flex gap-1">
|
|
<button className="btn btn-ghost" onClick={() => startEdit(t)} title="ویرایش">
|
|
<Pencil size={14} />
|
|
</button>
|
|
<button className="btn btn-danger" onClick={() => remove(t.id)} title="حذف">
|
|
<Trash2 size={14} />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={submit} className="card p-5">
|
|
<h2 className="font-bold mb-4 flex items-center gap-2">
|
|
{editingId ? (
|
|
<>
|
|
<Pencil size={18} className="text-[var(--gold)]" /> ویرایش تورنومنت #{editingId}
|
|
<button
|
|
type="button"
|
|
onClick={cancelEdit}
|
|
className="btn btn-ghost mr-auto flex items-center gap-1"
|
|
>
|
|
<X size={14} /> لغو
|
|
</button>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Plus size={18} className="text-[var(--gold)]" /> افزودن تورنومنت
|
|
</>
|
|
)}
|
|
</h2>
|
|
<div className="grid md:grid-cols-2 gap-4">
|
|
<label className="text-sm">
|
|
عنوان
|
|
<input
|
|
className="inp mt-1"
|
|
value={form.title}
|
|
onChange={(e) => setForm({ ...form, title: e.target.value })}
|
|
required
|
|
/>
|
|
</label>
|
|
<label className="text-sm">
|
|
ورودی (سکه)
|
|
<input
|
|
className="inp mt-1"
|
|
type="number"
|
|
value={form.entry_fee}
|
|
onChange={(e) => setForm({ ...form, entry_fee: +e.target.value })}
|
|
/>
|
|
</label>
|
|
<label className="text-sm md:col-span-2">
|
|
توضیح
|
|
<input
|
|
className="inp mt-1"
|
|
value={form.description}
|
|
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
|
/>
|
|
</label>
|
|
<label className="text-sm md:col-span-2">
|
|
جوایز (سکه، با کاما — نفر اول، دوم، …)
|
|
<input
|
|
className="inp mt-1"
|
|
placeholder="5000,3000,1000"
|
|
value={form.prizes}
|
|
onChange={(e) => setForm({ ...form, prizes: e.target.value })}
|
|
/>
|
|
</label>
|
|
<label className="text-sm">
|
|
شروع (تهران)
|
|
<input
|
|
className="inp mt-1"
|
|
type="datetime-local"
|
|
value={form.starts_at}
|
|
onChange={(e) => setForm({ ...form, starts_at: e.target.value })}
|
|
required
|
|
/>
|
|
</label>
|
|
<label className="text-sm">
|
|
پایان (تهران)
|
|
<input
|
|
className="inp mt-1"
|
|
type="datetime-local"
|
|
value={form.ends_at}
|
|
onChange={(e) => setForm({ ...form, ends_at: e.target.value })}
|
|
required
|
|
/>
|
|
</label>
|
|
</div>
|
|
<p className="text-xs text-[#8aa] mt-3">
|
|
برای فعالشدنِ فوری، زمانِ شروع را کمی قبل از الان بگذارید. برد ۱۰۰ و شرکت ۲۵ امتیاز.
|
|
</p>
|
|
<button className="btn btn-gold mt-4" disabled={busy}>
|
|
{busy ? "…" : editingId ? "ذخیره تغییرات" : "ساخت تورنومنت"}
|
|
</button>
|
|
</form>
|
|
</Shell>
|
|
);
|
|
}
|