chore: merge admin repo history into monorepo at admin/

git-subtree-dir: admin
git-subtree-mainline: 35f5084005
git-subtree-split: 9f1eccd2f7
This commit is contained in:
2026-09-27 10:30:47 +03:30
49 changed files with 11446 additions and 0 deletions
@@ -0,0 +1,162 @@
"use client";
import { useState } from "react";
import { ShieldAlert, ShieldCheck, Flag } from "lucide-react";
import { Button } from "@/components/ui/Button";
import { Modal } from "@/components/ui/Modal";
import { useToast } from "@/components/ui/Toast";
import { formatApiError } from "@/lib/hooks";
import { blockUser, unblockUser, reportUser, UserState } from "@/lib/api";
// دکمه‌های بلاک/آنبلاک/گزارش یک کاربر؛ هم توی لیست همه‌ی کاربران و هم توی
// صفحه‌ی جزئیات یک کاربر استفاده می‌شه.
export function UserModerationActions({
userId,
state,
onChanged,
}: {
userId: number;
state: UserState;
onChanged: () => void;
}) {
const toast = useToast();
const [blockOpen, setBlockOpen] = useState(false);
const [reportOpen, setReportOpen] = useState(false);
const [blockReason, setBlockReason] = useState("");
const [reportReason, setReportReason] = useState("");
const [loading, setLoading] = useState(false);
async function handleBlock() {
if (blockReason.trim().length < 3) return;
setLoading(true);
try {
await blockUser(userId, blockReason.trim());
toast.showSuccess("کاربر بلاک شد");
setBlockOpen(false);
setBlockReason("");
onChanged();
} catch (err) {
toast.showError(formatApiError(err));
} finally {
setLoading(false);
}
}
async function handleUnblock() {
setLoading(true);
try {
await unblockUser(userId);
toast.showSuccess("کاربر آنبلاک شد");
onChanged();
} catch (err) {
toast.showError(formatApiError(err));
} finally {
setLoading(false);
}
}
async function handleReport() {
if (reportReason.trim().length < 3) return;
setLoading(true);
try {
await reportUser(userId, reportReason.trim());
toast.showSuccess("گزارش ثبت شد");
setReportOpen(false);
setReportReason("");
onChanged();
} catch (err) {
toast.showError(formatApiError(err));
} finally {
setLoading(false);
}
}
return (
<>
<div className="flex items-center gap-2">
{state === "blocked" ? (
<Button
variant="tertiary"
size="sm"
icon={<ShieldCheck size={14} />}
isLoading={loading}
onClick={handleUnblock}
>
آنبلاک
</Button>
) : (
<Button
variant="tertiary"
size="sm"
icon={<ShieldAlert size={14} />}
className="!text-(--color-status-error) !border-red-200"
onClick={() => setBlockOpen(true)}
>
بلاک
</Button>
)}
<Button variant="ghost" size="sm" icon={<Flag size={14} />} onClick={() => setReportOpen(true)}>
گزارش
</Button>
</div>
<Modal open={blockOpen} onClose={() => setBlockOpen(false)} title="بلاک کردن کاربر">
<div className="flex flex-col gap-4">
<p className="text-[13px] text-(--color-content-secondary)">
کاربر بلاک‌شده دیگه نمی‌تونه وارد اپ بشه (حتی با توکن معتبر فعلیش).
</p>
<textarea
value={blockReason}
onChange={(e) => setBlockReason(e.target.value)}
rows={3}
placeholder="دلیل بلاک کردن..."
className="rounded-(--radius-sm) border-2 border-(--color-border-default) bg-(--color-surface-default) px-4 py-3 text-[14px] focus:outline-none focus:border-(--color-border-interactive) focus:ring-4 focus:ring-(--color-brand-100) resize-none"
/>
<div className="flex justify-end gap-2">
<Button variant="ghost" size="sm" onClick={() => setBlockOpen(false)}>
انصراف
</Button>
<Button
size="sm"
isLoading={loading}
disabled={blockReason.trim().length < 3}
className="!bg-(--color-status-error) !shadow-none hover:!bg-red-700"
onClick={handleBlock}
>
بلاک کن
</Button>
</div>
</div>
</Modal>
<Modal open={reportOpen} onClose={() => setReportOpen(false)} title="ثبت گزارش روی کاربر">
<div className="flex flex-col gap-4">
<p className="text-[13px] text-(--color-content-secondary)">
یک یادداشت نظارتی برای این کاربر ثبت می‌شه (مستقل از بلاک کردن)؛ برای پیگیری
سابقه‌ی شکایت‌ها.
</p>
<textarea
value={reportReason}
onChange={(e) => setReportReason(e.target.value)}
rows={3}
placeholder="دلیل گزارش..."
className="rounded-(--radius-sm) border-2 border-(--color-border-default) bg-(--color-surface-default) px-4 py-3 text-[14px] focus:outline-none focus:border-(--color-border-interactive) focus:ring-4 focus:ring-(--color-brand-100) resize-none"
/>
<div className="flex justify-end gap-2">
<Button variant="ghost" size="sm" onClick={() => setReportOpen(false)}>
انصراف
</Button>
<Button
size="sm"
isLoading={loading}
disabled={reportReason.trim().length < 3}
onClick={handleReport}
>
ثبت گزارش
</Button>
</div>
</div>
</Modal>
</>
);
}