gh_UserManager/apps/web/app/account/page.tsx
bermooda-company 54d5891edf user
2026-08-23 23:59:14 +03:30

704 lines
32 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { Users, Shield, Building2, Settings, LogOut, User, Key, KeyRound, Smartphone, Calendar, MapPin, Image as ImageIcon, Briefcase, Mail, ChevronLeft, ChevronRight } from "lucide-react";
import { useLanguage } from "@/components/language-provider";
import { useAuth } from "@/components/auth/auth-provider";
import { RouteGuard } from "@/components/auth/route-guard";
import { getAccessToken, apiFetch } from "@/lib/api";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { registerPasskey } from "@/lib/webauthn";
interface UserAccount {
user_id: string;
email: string;
username?: string | null;
full_name: string | null;
given_name?: string | null;
family_name?: string | null;
trust_state?: string | null;
trust_score?: number | null;
skills?: string[];
birth_date?: string | null;
province?: string | null;
city?: string | null;
gender?: string | null;
avatar_url?: string | null;
status: string;
is_active: boolean;
mfa_enabled: boolean;
email_verified: boolean;
phone_verified: boolean;
phone: string | null;
created_at: string;
last_login_at: string | null;
passkeys?: any[];
sessions?: any[];
}
const SECTIONS = [
{
key: "identity",
labelFa: "هویت",
labelEn: "Identity",
icon: Users,
descFa: "اطلاعات هویتی و پروفایل عمومی حساب",
descEn: "Identity info and public profile",
},
{
key: "auth",
labelFa: "احراز هویت",
labelEn: "Authentication",
icon: Shield,
descFa: "اطلاعات ورود، دستگاه‌ها و امنیت حسابتان را مدیریت کنید",
descEn: "Manage sign-in methods, devices and security",
},
{
key: "businesses",
labelFa: "کسب‌وکارها",
labelEn: "Businesses",
icon: Building2,
descFa: "سازمان‌ها و کسب‌وکارهای متصل به حساب شما",
descEn: "Organizations connected to your account",
},
{
key: "bizManage",
labelFa: "مدیریت کسب‌وکار",
labelEn: "Business Management",
icon: Settings,
descFa: "ساخت، ویرایش و انتقال مالکیت کسب‌وکارها",
descEn: "Create, edit and transfer businesses",
},
];
export default function AccountCenterPage() {
const { lang } = useLanguage();
const t = (fa: string, en: string) => (lang === "fa" ? fa : en);
const Chev = lang === "fa" ? ChevronLeft : ChevronRight;
const { user: ctxUser, logout } = useAuth();
const [mounted, setMounted] = useState(false);
const token = getAccessToken();
const [activeTab, setActiveTab] = useState("identity");
const [loading, setLoading] = useState(false);
const [user, setUser] = useState<UserAccount | null>(null);
const [orgs, setOrgs] = useState<any[]>([]);
const [sessions, setSessions] = useState<any[]>([]);
const [products, setProducts] = useState<any[]>([]);
const [pkBusy, setPkBusy] = useState(false);
const [pkError, setPkError] = useState("");
const [editing, setEditing] = useState(false);
const [saving, setSaving] = useState(false);
const [form, setForm] = useState<Record<string, string>>({});
const startEdit = () => {
setForm({
full_name: user?.full_name || "",
given_name: user?.given_name || "",
family_name: user?.family_name || "",
username: user?.username || "",
province: user?.province || "",
city: user?.city || "",
gender: user?.gender || "",
birth_date: user?.birth_date || "",
skills: (user?.skills || []).join(", "),
});
setEditing(true);
};
const cancelEdit = () => setEditing(false);
const saveEdit = async () => {
setSaving(true);
try {
const payload: any = { ...form };
payload.skills = form.skills
? form.skills
.split(",")
.map((s: string) => s.trim())
.filter(Boolean)
: [];
const res = await apiFetch("/auth/me/", {
method: "PATCH",
body: JSON.stringify(payload),
});
if (res.ok) {
setUser(await res.json());
setEditing(false);
}
} finally {
setSaving(false);
}
};
useEffect(() => {
setMounted(true);
}, []);
// Keep the active tab in sync with the ?tab= query param so the URL reflects
// the selected sidebar section and supports back/forward navigation.
useEffect(() => {
const valid = new Set(SECTIONS.map((s) => s.key));
const syncFromUrl = () => {
const tab = new URLSearchParams(window.location.search).get("tab");
if (tab && valid.has(tab)) setActiveTab(tab);
};
syncFromUrl();
window.addEventListener("popstate", syncFromUrl);
return () => window.removeEventListener("popstate", syncFromUrl);
}, []);
useEffect(() => {
if (ctxUser && !user) setUser(ctxUser as unknown as UserAccount);
}, [ctxUser, user]);
useEffect(() => {
const fetchData = async () => {
if (!token) return;
setLoading(true);
try {
const userRes = await apiFetch("/auth/me/");
if (userRes.ok) setUser(await userRes.json());
const orgRes = await apiFetch("/organizations/");
if (orgRes.ok) {
const d = await orgRes.json();
setOrgs((d as any).results || d || []);
}
const sessRes = await apiFetch("/sessions/");
if (sessRes.ok) {
const d = await sessRes.json();
setSessions(Array.isArray(d) ? d : d.results || []);
}
const prodRes = await apiFetch("/products/");
if (prodRes.ok) {
const d = await prodRes.json();
setProducts(Array.isArray(d) ? d : d.results || []);
}
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
fetchData();
}, [token]);
const handleLogout = async () => {
await logout();
window.location.href = `/login?next=${encodeURIComponent(window.location.pathname)}`;
};
const handleAddPasskey = async () => {
if (!token) return;
setPkBusy(true);
setPkError("");
try {
await registerPasskey(token, "My Passkey");
const userRes = await apiFetch("/auth/me/");
if (userRes.ok) setUser(await userRes.json());
} catch (e: any) {
setPkError(e?.message || t("ثبت پاسکلی شکست خورد", "Passkey registration failed"));
} finally {
setPkBusy(false);
}
};
if (!mounted) return <div className="min-h-screen bg-[#f6f7fb]" />;
const current = SECTIONS.find((s) => s.key === activeTab);
return (
<RouteGuard>
<div dir={lang === "fa" ? "rtl" : "ltr"} className="min-h-screen bg-[#f6f7fb]">
<aside
className={cn(
"fixed inset-y-0 z-40 flex w-64 flex-col overflow-y-auto border-slate-200 bg-white",
lang === "fa" ? "right-0 border-l" : "left-0 border-r"
)}
>
<div className="flex items-center gap-2.5 border-b border-slate-100 px-5 py-4">
<span className="flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-[#8b7cf6] to-[#6d5ef0] text-white shadow-md">
<Users className="h-5 w-5" />
</span>
<span className="text-sm font-bold text-slate-900">UserManager</span>
</div>
<nav className="flex-1 space-y-1 p-3">
{SECTIONS.map((sec) => {
const isActive = activeTab === sec.key;
return (
<button
key={sec.key}
onClick={() => {
setActiveTab(sec.key);
window.history.pushState(
{},
"",
`${window.location.pathname}?tab=${sec.key}`,
);
}}
className={cn(
"flex w-full items-center gap-3 rounded-xl px-3.5 py-2.5 text-sm font-medium transition",
isActive
? "bg-violet-50 font-semibold text-[#6d5ef0]"
: "text-slate-500 hover:bg-slate-50 hover:text-slate-700"
)}
>
<sec.icon className="h-[18px] w-[18px]" />
{t(sec.labelFa, sec.labelEn)}
</button>
);
})}
</nav>
<div className="space-y-3 border-t border-slate-100 p-4">
<div className="flex items-center justify-between px-1">
<span className="text-xs text-slate-400">{lang === "fa" ? "زبان" : "Language"}</span>
<button
onClick={() => {
localStorage.setItem("language", lang === "fa" ? "en" : "fa");
window.location.reload();
}}
className="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-600 transition hover:bg-slate-200"
>
{lang === "fa" ? "EN" : "فا"}
</button>
</div>
<button
onClick={handleLogout}
className="flex w-full items-center gap-2 rounded-xl px-3.5 py-2 text-sm text-rose-500 transition hover:bg-rose-50"
>
<LogOut className="h-4 w-4 rtl:rotate-180" />
{t("خروج از حساب", "Log out")}
</button>
</div>
</aside>
<main className={cn("min-h-screen p-6 lg:p-10", lang === "fa" ? "pr-64 lg:pr-72" : "pl-64 lg:pl-72")}>
<header className="mb-8">
<h1 className="text-2xl font-extrabold tracking-tight text-[#1c1d3a]">{current ? t(current.labelFa, current.labelEn) : ""}</h1>
<p className="mt-1.5 text-sm text-slate-500">{current ? t(current.descFa, current.descEn) : ""}</p>
</header>
{activeTab === "identity" && (
<div className="grid gap-6 lg:grid-cols-3">
{/* Profile summary card */}
<Card className="flex flex-col items-center py-8 text-center lg:col-span-1">
{user?.avatar_url ? (
<img src={user.avatar_url} alt="" className="h-24 w-24 rounded-full object-cover ring-4 ring-violet-50" />
) : (
<span className="flex h-24 w-24 items-center justify-center rounded-full bg-violet-50 text-[#6d5ef0] ring-4 ring-violet-50">
<User className="h-11 w-11" />
</span>
)}
<div className="mt-4 text-lg font-bold text-slate-900">{user?.full_name || "-"}</div>
<div className="mt-0.5 text-sm text-slate-500">{user?.email || ""}</div>
{user?.status === "active" && (
<span className="mt-3 inline-flex items-center gap-1.5 rounded-full bg-emerald-50 px-3 py-1 text-xs font-medium text-emerald-600">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
{t("فعال", "Active")}
</span>
)}
<div className="mt-3 flex flex-wrap items-center justify-center gap-2">
{typeof user?.mfa_enabled === "boolean" && (
<span
className={`inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium ${
user.mfa_enabled
? "bg-emerald-50 text-emerald-600"
: "bg-amber-50 text-amber-600"
}`}
>
<span className="h-1.5 w-1.5 rounded-full bg-current" />
{user.mfa_enabled
? t("احراز دومرحله‌ای فعال", "MFA enabled")
: t("احراز دومرحله‌ای غیرفعال", "MFA disabled")}
</span>
)}
{user?.trust_state && (
<span className="inline-flex items-center gap-1.5 rounded-full bg-violet-50 px-3 py-1 text-xs font-medium text-[#6d5ef0]">
{t("سطح اعتماد", "Trust")}: {user.trust_state}
{typeof user.trust_score === "number" ? ` (${user.trust_score})` : ""}
</span>
)}
</div>
<button className="mt-5 rounded-full border border-slate-200 px-4 py-1.5 text-xs font-medium text-slate-600 transition hover:border-[#6d5ef0]/40 hover:text-[#6d5ef0]">
{t("ویرایش تصویر", "Edit photo")}
</button>
</Card>
{/* Personal info card */}
<Card className="lg:col-span-2">
<div className="flex items-start justify-between gap-4">
<h3 className="text-base font-bold text-slate-900">{t("اطلاعات شخصی", "Personal information")}</h3>
{editing ? (
<div className="flex shrink-0 gap-2">
<button
onClick={cancelEdit}
className="rounded-full border border-slate-200 px-3.5 py-1.5 text-xs font-medium text-slate-500 transition hover:border-slate-300"
>
{t("انصراف", "Cancel")}
</button>
<button
onClick={saveEdit}
disabled={saving}
className="rounded-full bg-[#6d5ef0] px-3.5 py-1.5 text-xs font-semibold text-white shadow-[0_6px_18px_rgba(109,94,240,0.35)] transition hover:bg-[#5747d8] disabled:opacity-50"
>
{saving ? "..." : t("ذخیره", "Save")}
</button>
</div>
) : (
<button
onClick={startEdit}
className="shrink-0 rounded-full border border-slate-200 px-3.5 py-1.5 text-xs font-medium text-slate-500 transition hover:border-[#6d5ef0]/40 hover:text-[#6d5ef0]"
>
{t("ویرایش اطلاعات", "Edit")}
</button>
)}
</div>
{loading ? (
<Loading />
) : editing ? (
<div className="mt-4 grid gap-x-8 gap-y-4 sm:grid-cols-2">
<EditField label={t("نام کامل", "Full name")} value={form.full_name} onChange={(v: string) => setForm({ ...form, full_name: v })} />
<EditField label={t("نام", "First name")} value={form.given_name} onChange={(v: string) => setForm({ ...form, given_name: v })} />
<EditField label={t("نام خانوادگی", "Last name")} value={form.family_name} onChange={(v: string) => setForm({ ...form, family_name: v })} />
<EditField label={t("نام کاربری", "Username")} value={form.username} onChange={(v: string) => setForm({ ...form, username: v })} />
<EditField label={t("استان", "Province")} value={form.province} onChange={(v: string) => setForm({ ...form, province: v })} />
<EditField label={t("شهر", "City")} value={form.city} onChange={(v: string) => setForm({ ...form, city: v })} />
<EditField
label={t("جنسیت", "Gender")}
value={form.gender}
onChange={(v: string) => setForm({ ...form, gender: v })}
select
options={[
{ value: "", label: t("انتخاب نشده", "Not set") },
{ value: "male", label: t("مرد", "Male") },
{ value: "female", label: t("زن", "Female") },
]}
/>
<EditField label={t("تاریخ تولد", "Birth date")} type="date" value={form.birth_date} onChange={(v: string) => setForm({ ...form, birth_date: v })} />
<EditField
label={t("مهارت‌ها", "Skills")}
value={form.skills}
onChange={(v: string) => setForm({ ...form, skills: v })}
full
hint={t("با کاما جدا کنید", "Comma separated")}
/>
</div>
) : (
<div className="mt-4 grid gap-x-8 gap-y-4 sm:grid-cols-2">
<Field label={t("نام", "First name")} value={user?.given_name || user?.full_name?.split(" ")[0] || "-"} />
<Field label={t("نام خانوادگی", "Last name")} value={user?.family_name || user?.full_name?.split(" ").slice(1).join(" ") || "-"} />
<Field label={t("نام کاربری", "Username")} value={user?.username || "-"} />
<Field label={t("ایمیل", "Email")} value={user?.email || "-"} />
<Field label={t("شماره موبایل", "Phone")} value={user?.phone || "-"} />
<Field label={t("شهر / استان", "City / Province")} value={[user?.city, user?.province].filter(Boolean).join(" / ") || "-"} icon={MapPin} />
<Field label={t("جنسیت", "Gender")} value={genderLabel(user?.gender, t)} />
<Field label={t("تاریخ تولد", "Birth date")} value={user?.birth_date ? new Date(user.birth_date).toLocaleDateString(lang === "fa" ? "fa-IR" : "en-US") : "-"} icon={Calendar} />
<Field label={t("مهارت‌ها", "Skills")} value={user?.skills?.length ? user.skills.join("، ") : "-"} full />
</div>
)}
</Card>
</div>
)}
{activeTab === "auth" && (
<div className="grid gap-6 lg:grid-cols-2">
<Card>
<CardHead title={t("روش‌های ورود", "Sign-in methods")} />
<div className="mt-3 divide-y divide-slate-100">
<Row icon={Smartphone} title={t("شماره موبایل", "Phone number")} sub={user?.phone || "-"} verified={user?.phone_verified} chev />
<Row icon={Mail} title={t("ایمیل", "Email")} sub={user?.email || "-"} verified={user?.email_verified} chev />
</div>
<CardHead classTop="mt-6" title={t("تنظیمات امنیتی", "Security settings")} />
<div className="mt-3 divide-y divide-slate-100">
<Row icon={Shield} title={t("احراز هویت دومرحله‌ای", "Two-factor authentication")} toggleOn={!!user?.mfa_enabled} />
<Row icon={Key} title={t("ورود با دستگاه قابل‌اعتماد", "Trusted device login")} toggleOn />
<Row icon={KeyRound} title={t("مدیریت دستگاه‌ها", "Manage devices")} button={t("مدیریت", "Manage")} onClick={handleAddPasskey} busy={pkBusy} />
</div>
{pkError && <p className="mt-3 text-xs text-rose-500">{pkError}</p>}
</Card>
<Card>
<CardHead title={t("نشست‌ها و دستگاه‌ها", "Sessions & devices")} />
<div className="mt-3 divide-y divide-slate-100">
{(sessions || []).slice(0, 5).map((s: any, i: number) => (
<div key={i} className="flex items-center gap-3 py-3.5">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-slate-50 text-slate-500">
<Smartphone className="h-5 w-5" />
</span>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium text-slate-800">{s.device_name || t("دستگاه ناشناس", "Unknown device")}</div>
<div className="text-xs text-slate-400">
{s.ip_address || "—"} ·{" "}
{s.expires_at ? new Date(s.expires_at).toLocaleDateString(lang === "fa" ? "fa-IR" : "en-US") : "—"}
</div>
</div>
<span className={cn("inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-[11px] font-medium", s.status === "active" ? "bg-emerald-50 text-emerald-600" : "bg-slate-100 text-slate-500")}>
<span className={cn("h-1.5 w-1.5 rounded-full", s.status === "active" ? "bg-emerald-500" : "bg-slate-400")} />
{s.status === "active" ? t("فعال", "Active") : (s.status || t("غیرفعال", "Inactive"))}
</span>
<button className="rounded-lg p-1.5 text-slate-300 transition hover:bg-slate-50 hover:text-slate-500">
<Chev className="h-4 w-4 rtl:rotate-180" />
</button>
</div>
))}
{(!sessions || sessions.length === 0) && !loading && (
<p className="py-6 text-center text-sm text-slate-400">{t("نشست فعالی یافت نشد", "No active sessions found")}</p>
)}
</div>
<button className="mt-3 w-full rounded-xl border border-slate-200 py-2.5 text-xs font-medium text-slate-600 transition hover:border-[#6d5ef0]/40 hover:text-[#6d5ef0]">
{t("خروج از تمام نشست‌ها", "Sign out of all sessions")}
</button>
</Card>
</div>
)}
{activeTab === "businesses" && (
<Card>
<CardHead title={t("سازمان‌های من", "My organizations")} action={t("عضویت در سازمان", "Join organization")} />
{loading ? (
<Loading />
) : orgs.length === 0 ? (
<Empty text={t("هنوز عضو هیچ کسب‌وکاری نیستید", "You haven't joined any business yet")} icon={Building2} />
) : (
<div className="mt-4 divide-y divide-slate-100">
{orgs.map((o: any, i: number) => (
<div key={o.id || i} className="flex items-center gap-4 py-4">
<span className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-violet-50 text-lg font-extrabold text-[#6d5ef0]">
{(o.name || "?").charAt(0)}
</span>
<div className="min-w-0 flex-1">
<div className="text-sm font-semibold text-slate-900">{o.name || o.slug}</div>
<div className="mt-0.5 flex items-center gap-2 text-xs text-slate-400">
<span className="inline-flex items-center gap-1 rounded-full bg-violet-50 px-2 py-0.5 font-medium text-[#6d5ef0]">
{roleLabel(o.role, t)}
</span>
<span>{t("عضو", "Member")}</span>
</div>
</div>
<div className="hidden text-left text-xs text-slate-400 sm:block" dir="ltr">
{o.joined_at ? new Date(o.joined_at).toLocaleDateString(lang === "fa" ? "fa-IR" : "en-US") : ""}
</div>
<button className="rounded-lg p-1.5 text-slate-300 transition hover:bg-slate-50 hover:text-slate-500">
<Chev className="h-4 w-4 rtl:rotate-180" />
</button>
</div>
))}
</div>
)}
</Card>
)}
{activeTab === "bizManage" && (
<div className="grid gap-6">
<Card>
<CardHead title={t("مدیریت کسب‌وکارها", "Business management")} />
<div className="mt-4 grid gap-4 sm:grid-cols-2">
<ActionCard icon={Briefcase} title={t("ساخت کسب‌وکار جدید", "Create new business")} desc={t("نام برند، صنعت، استان و اطلاعات پایه را وارد کنید", "Enter brand name, industry, province and basics")} cta={t("+ ساخت کسب‌وکار جدید", "+ Create business")} primary />
<ActionCard icon={Settings} title={t("ویرایش کسب‌وکار", "Edit business")} desc={t("اطلاعات پایه، تماس و اطلاعات حقوقی را ویرایش کنید", "Edit basic, contact and legal information")} cta={t("انتخاب کسب‌وکار", "Pick a business")} />
</div>
</Card>
<Card>
<CardHead title={t("محصولات من", "My products")} desc={t("دسترسی به محصولات متصل به این حساب", "Access connected products of this account")} />
<div className="mt-4 space-y-3">
{products.length === 0 ? (
<p className="py-6 text-center text-sm text-slate-400">{t("هنوز به محصولی دسترسی ندارید", "You don't have access to any product yet")}</p>
) : (
products.map((p: any) => (
<div key={p.id || p.key} className="flex items-center gap-4 rounded-2xl border border-slate-100 p-4">
<span className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-violet-50 text-base font-extrabold text-[#6d5ef0]">
{(p.name || p.key || "?").charAt(0)}
</span>
<div className="min-w-0 flex-1">
<div className="text-sm font-semibold text-slate-900">{p.name || p.key}</div>
<div className="text-xs text-slate-400">{p.description || p.key}</div>
</div>
<Button variant="ghost" size="sm" className="rounded-full border border-rose-100 text-xs text-rose-500 hover:bg-rose-50">
{t("حذف دسترسی", "Revoke access")}
</Button>
</div>
))
)}
</div>
<div className="mt-4 rounded-2xl bg-slate-50 p-4 text-xs leading-5 text-slate-400">
{t(
"دسترسی به محصولات تابع سیاست دسترسی کلی است.",
"Product access follows the global access policy."
)}
</div>
</Card>
</div>
)}
<footer className="mt-14 pb-6 text-center text-xs text-slate-300">
© {new Date().getFullYear()} UserManager · MyAccount Hamsoo
</footer>
</main>
</div>
</RouteGuard>
);
}
function Card({ children, className }: { children: React.ReactNode; className?: string }) {
return (
<div className={cn("rounded-3xl border border-slate-100 bg-white p-6 shadow-[0_1px_3px_rgba(16,24,40,0.05)] sm:p-7", className)}>
{children}
</div>
);
}
function CardHead({ title, desc, action, classTop }: { title: string; desc?: string; action?: string; classTop?: string }) {
return (
<div className={cn("flex items-start justify-between gap-4", classTop)}>
<div>
<h3 className="text-base font-bold text-slate-900">{title}</h3>
{desc && <p className="mt-0.5 text-xs text-slate-400">{desc}</p>}
</div>
{action && (
<button className="shrink-0 rounded-full border border-slate-200 px-3.5 py-1.5 text-xs font-medium text-slate-500 transition hover:border-[#6d5ef0]/40 hover:text-[#6d5ef0]">
{action}
</button>
)}
</div>
);
}
function Field({ label, value, icon: Icon, full }: any) {
return (
<div className={cn(full && "sm:col-span-2")}>
<dt className="text-xs font-medium text-slate-400">{label}</dt>
<dd className="mt-1 flex items-center gap-1.5 text-sm font-medium text-slate-800">
{Icon && <Icon className="h-3.5 w-3.5 text-slate-300" />}
{value}
</dd>
</div>
);
}
function EditField({
label,
value,
onChange,
type,
full,
hint,
select,
options,
}: any) {
const base =
"w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800 outline-none transition focus:border-[#6d5ef0]/50 focus:ring-2 focus:ring-[#6d5ef0]/15";
return (
<div className={cn(full && "sm:col-span-2")}>
<label className="mb-1.5 block text-xs font-medium text-slate-400">{label}</label>
{select ? (
<select className={base} value={value} onChange={(e) => onChange(e.target.value)}>
{(options || []).map((o: any) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
) : (
<input
type={type || "text"}
className={base}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
)}
{hint && <p className="mt-1 text-[11px] text-slate-400">{hint}</p>}
</div>
);
}
function Row({ icon: Icon, title, sub, verified, toggleOn, chev, button, onClick, busy }: any) {
return (
<div className="flex items-center gap-3 py-3.5">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-slate-50 text-slate-500">
<Icon className="h-5 w-5" />
</span>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-slate-800">{title}</div>
{sub && <div className="truncate text-xs text-slate-400">{sub}</div>}
</div>
{verified !== undefined && (
<span className={cn("inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-[11px] font-medium", verified ? "bg-emerald-50 text-emerald-600" : "bg-amber-50 text-amber-600")}>
{!verified && "! "}
{verified ? "تأیید شده ✓" : "تأیید نشده"}
</span>
)}
{toggleOn !== undefined && (
<span className={cn("relative inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full transition", toggleOn ? "bg-[#6d5ef0]" : "bg-slate-200")}>
<span className={cn("absolute h-5 w-5 rounded-full bg-white shadow transition-all", toggleOn ? "right-[2px] rtl:left-[2px] rtl:right-auto" : "left-[2px]")} />
</span>
)}
{chev && <ChevronRight className="h-4 w-4 text-slate-300 rtl:rotate-180" />}
{button && (
<button onClick={onClick} disabled={busy} className="rounded-full border border-slate-200 px-3 py-1 text-xs text-slate-500 transition hover:border-[#6d5ef0]/40 hover:text-[#6d5ef0] disabled:opacity-50">
{busy ? "..." : button}
</button>
)}
</div>
);
}
function ActionCard({ icon: Icon, title, desc, cta, primary }: any) {
return (
<div className="rounded-2xl border border-slate-100 p-5 transition hover:shadow-md">
<span className="flex h-11 w-11 items-center justify-center rounded-xl bg-violet-50 text-[#6d5ef0]">
<Icon className="h-5 w-5" />
</span>
<div className="mt-3 text-sm font-bold text-slate-900">{title}</div>
<p className="mt-1 text-xs leading-5 text-slate-400">{desc}</p>
<button
className={cn(
"mt-4 rounded-full px-4 py-1.5 text-xs font-semibold transition",
primary
? "bg-[#6d5ef0] text-white shadow-[0_6px_18px_rgba(109,94,240,0.35)] hover:bg-[#5747d8]"
: "border border-slate-200 text-slate-600 hover:border-[#6d5ef0]/40 hover:text-[#6d5ef0]"
)}
>
{cta}
</button>
</div>
);
}
function Empty({ text, icon: Icon }: any) {
return (
<div className="flex flex-col items-center py-14 text-center">
<span className="flex h-14 w-14 items-center justify-center rounded-2xl bg-slate-50 text-slate-300">
<Icon className="h-7 w-7" />
</span>
<p className="mt-3 text-sm text-slate-400">{text}</p>
</div>
);
}
function Loading() {
return (
<div className="animate-pulse space-y-3 py-6">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-10 rounded-xl bg-slate-50" />
))}
</div>
);
}
function genderLabel(g: string | null | undefined, t: (fa: string, en: string) => string) {
if (!g) return "-";
if (g === "male") return t("مرد", "Male");
if (g === "female") return t("زن", "Female");
return g;
}
function roleLabel(r: string | undefined | null, t: (fa: string, en: string) => string) {
const v = (r || "").toLowerCase();
if (v === "owner") return t("مالک", "Owner");
if (v === "admin") return t("مدیر", "Admin");
return t("عضو", "Member");
}