first commit
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user