Add structured rejection reasons (checkable presets + free text)

The reject modal now offers 5 common rejection reasons as tickable
options (no photo, incomplete info, looks fake/bot, inappropriate
photos, contact info in profile), combined with an optional free-text
note. The combined string is sent as the existing reason field — no
backend change needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 09:23:11 +03:30
parent 665838bbe7
commit 7cf6c78c77
+64 -9
View File
@@ -13,6 +13,7 @@ import {
Heart,
Sparkles,
Search,
Check,
} from "lucide-react";
import { Card } from "@/components/ui/Card";
import { Avatar } from "@/components/ui/Avatar";
@@ -260,6 +261,15 @@ export default function UserReviewPage() {
);
}
// دلایل رایج رد پروفایل — ادمین تیک می‌زنه، متن نهایی خودکار برای کاربر ساخته می‌شه
const COMMON_REASONS = [
"عکس پروفایل نداره یا واضح نیست",
"اطلاعات پروفایل ناقصه",
"پروفایل به نظر جعلی یا ربات میاد",
"محتوای عکس‌ها نامناسبه",
"توی پروفایل شماره تماس یا لینک گذاشته",
];
function RejectModal({
open,
onClose,
@@ -271,14 +281,25 @@ function RejectModal({
onConfirm: (reason: string) => void;
loading: boolean;
}) {
const [reason, setReason] = useState("");
const [selected, setSelected] = useState<string[]>([]);
const [extra, setExtra] = useState("");
useEffect(() => {
if (open) {
// eslint-disable-next-line react-hooks/set-state-in-effect
if (open) setReason("");
setSelected([]);
setExtra("");
}
}, [open]);
const valid = reason.trim().length >= 3;
function toggle(text: string) {
setSelected((prev) =>
prev.includes(text) ? prev.filter((t) => t !== text) : [...prev, text]
);
}
const finalReason = [...selected, extra.trim()].filter(Boolean).join("؛ ");
const valid = finalReason.length >= 3;
return (
<Modal open={open} onClose={onClose} title="رد کردن کاربر">
@@ -287,15 +308,49 @@ function RejectModal({
<label className="text-[13px] font-semibold text-(--color-content-secondary) px-1">
دلیل رد (برای کاربر نمایش داده می‌شه)
</label>
<div className="flex flex-col gap-2">
{COMMON_REASONS.map((text) => {
const checked = selected.includes(text);
return (
<button
key={text}
type="button"
onClick={() => toggle(text)}
className={`flex items-center gap-2.5 rounded-(--radius-sm) border-2 px-3.5 py-2.5 text-right text-[13px] font-medium transition-colors ${
checked
? "border-(--color-brand-500) bg-(--color-surface-interactive) text-(--color-brand-700)"
: "border-(--color-border-default) text-(--color-content-secondary)"
}`}
>
<span
className={`h-5 w-5 rounded-(--radius-xs) flex items-center justify-center shrink-0 border-2 ${
checked
? "bg-(--color-brand-500) border-(--color-brand-500)"
: "border-(--color-border-default)"
}`}
>
{checked && <Check size={13} className="text-white" strokeWidth={3} />}
</span>
{text}
</button>
);
})}
</div>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-[13px] font-semibold text-(--color-content-secondary) px-1">
توضیح تکمیلی (اختیاری)
</label>
<textarea
value={reason}
onChange={(e) => setReason(e.target.value)}
rows={4}
placeholder="مثلاً: عکس پروفایل نداره یا اطلاعات ناقصه"
value={extra}
onChange={(e) => setExtra(e.target.value)}
rows={2}
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"
autoFocus
/>
</div>
<div className="flex justify-end gap-2 pt-2">
<Button variant="ghost" size="sm" onClick={onClose}>
انصراف
@@ -305,7 +360,7 @@ function RejectModal({
isLoading={loading}
disabled={!valid}
className="!bg-(--color-status-error) !shadow-none hover:!bg-red-700"
onClick={() => onConfirm(reason.trim())}
onClick={() => onConfirm(finalReason)}
>
رد کردن کاربر
</Button>