TorbatYar/frontend/components/settings/AccountSettings.tsx
Mortezakoohjani 800b0ba2c5 Deploy TorbatYar for torbatyar.ir with nginx multi-tenant routing.
Wire production domain, CORS for tenant subdomains, celery volume mounts, and nginx reverse proxy configs for apex, API, identity, auth, and wildcard tenants.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 21:43:33 +03:30

332 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { FormEvent, useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { getStoredToken } from "@/lib/auth";
import { identityApi, type AccountProfile } from "@/lib/identity";
import { Button, Container } from "@/components/ui";
const inputClass =
"mt-1 w-full rounded-xl border border-gray-200 px-4 py-2.5 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary disabled:bg-gray-50";
const labelClass = "block text-sm font-medium text-secondary";
interface AccountSettingsProps {
backHref?: string;
backLabel?: string;
}
export function AccountSettings({ backHref, backLabel }: AccountSettingsProps) {
const [profile, setProfile] = useState<AccountProfile | null>(null);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState("");
const load = useCallback(async () => {
const token = getStoredToken();
if (!token) {
setLoadError("توکن احراز هویت یافت نشد. لطفاً دوباره وارد شوید.");
setLoading(false);
return;
}
setLoading(true);
setLoadError("");
try {
const data = await identityApi.profile(token);
setProfile(data);
} catch (err) {
setLoadError(err instanceof Error ? err.message : "دریافت پروفایل ناموفق بود");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void load();
}, [load]);
return (
<Container className="py-10">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-secondary">تنظیمات حساب</h1>
<p className="mt-1 text-sm text-gray-600">
مدیریت پروفایل و رمز عبور حساب کاربری شما
</p>
</div>
{backHref && (
<Link href={backHref}>
<Button variant="outline" size="sm">
{backLabel || "بازگشت"}
</Button>
</Link>
)}
</div>
{loadError && (
<p className="mt-6 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-600">
{loadError}
</p>
)}
{loading ? (
<p className="mt-8 text-sm text-gray-500">در حال بارگذاری...</p>
) : profile ? (
<div className="mt-8 grid grid-cols-1 gap-6 lg:grid-cols-2">
<ProfileForm profile={profile} onUpdated={setProfile} />
<PasswordForm />
</div>
) : null}
</Container>
);
}
function ProfileForm({
profile,
onUpdated,
}: {
profile: AccountProfile;
onUpdated: (p: AccountProfile) => void;
}) {
const [displayName, setDisplayName] = useState(profile.display_name || "");
const [email, setEmail] = useState(profile.email || "");
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError("");
setSuccess("");
const token = getStoredToken();
if (!token) {
setError("توکن احراز هویت یافت نشد.");
return;
}
setSaving(true);
try {
const updated = await identityApi.updateProfile(token, {
display_name: displayName.trim() || null,
email: email.trim() || undefined,
});
onUpdated(updated);
setSuccess("پروفایل با موفقیت ذخیره شد.");
} catch (err) {
setError(err instanceof Error ? err.message : "ذخیره پروفایل ناموفق بود");
} finally {
setSaving(false);
}
};
return (
<form
onSubmit={handleSubmit}
className="rounded-2xl border border-gray-100 bg-white p-6 shadow-sm"
>
<h2 className="text-lg font-bold text-secondary">اطلاعات پروفایل</h2>
<p className="mt-1 text-xs text-gray-500">
نام نمایشی و ایمیل خود را ویرایش کنید.
</p>
<div className="mt-6 space-y-4">
<div>
<label className={labelClass} htmlFor="display_name">
نام نمایشی
</label>
<input
id="display_name"
className={inputClass}
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="نام و نام خانوادگی"
maxLength={255}
/>
</div>
<div>
<label className={labelClass} htmlFor="email">
ایمیل
</label>
<input
id="email"
type="email"
dir="ltr"
className={inputClass}
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
/>
</div>
<div>
<label className={labelClass}>شماره موبایل</label>
<input
className={`${inputClass} font-mono`}
dir="ltr"
value={profile.mobile || "—"}
disabled
readOnly
/>
<p className="mt-1 text-xs text-gray-400">
{profile.mobile_verified
? "شماره موبایل تأیید شده است."
: "برای تغییر شماره موبایل با پشتیبانی تماس بگیرید."}
</p>
</div>
{profile.username && (
<div>
<label className={labelClass}>نام کاربری</label>
<input
className={`${inputClass} font-mono`}
dir="ltr"
value={profile.username}
disabled
readOnly
/>
</div>
)}
</div>
{error && (
<p className="mt-4 rounded-lg bg-red-50 px-3 py-2 text-sm text-red-600">
{error}
</p>
)}
{success && (
<p className="mt-4 rounded-lg bg-green-50 px-3 py-2 text-sm text-green-700">
{success}
</p>
)}
<div className="mt-6">
<Button type="submit" loading={saving} loadingText="در حال ذخیره...">
ذخیره تغییرات
</Button>
</div>
</form>
);
}
function PasswordForm() {
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError("");
setSuccess("");
if (newPassword.length < 8) {
setError("رمز عبور جدید باید حداقل ۸ کاراکتر باشد.");
return;
}
if (newPassword !== confirmPassword) {
setError("رمز عبور جدید و تکرار آن یکسان نیستند.");
return;
}
const token = getStoredToken();
if (!token) {
setError("توکن احراز هویت یافت نشد.");
return;
}
setSaving(true);
try {
await identityApi.changePassword(token, {
current_password: currentPassword.trim() || undefined,
new_password: newPassword,
});
setSuccess("رمز عبور با موفقیت به‌روزرسانی شد.");
setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
} catch (err) {
setError(err instanceof Error ? err.message : "تغییر رمز عبور ناموفق بود");
} finally {
setSaving(false);
}
};
return (
<form
onSubmit={handleSubmit}
className="rounded-2xl border border-gray-100 bg-white p-6 shadow-sm"
>
<h2 className="text-lg font-bold text-secondary">تغییر رمز عبور</h2>
<p className="mt-1 text-xs text-gray-500">
اگر تاکنون رمز عبوری تنظیم نکردهاید، فیلد رمز فعلی را خالی بگذارید.
</p>
<div className="mt-6 space-y-4">
<div>
<label className={labelClass} htmlFor="current_password">
رمز عبور فعلی (اختیاری)
</label>
<input
id="current_password"
type="password"
dir="ltr"
className={inputClass}
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
autoComplete="current-password"
/>
</div>
<div>
<label className={labelClass} htmlFor="new_password">
رمز عبور جدید
</label>
<input
id="new_password"
type="password"
dir="ltr"
className={inputClass}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
autoComplete="new-password"
placeholder="حداقل ۸ کاراکتر"
/>
</div>
<div>
<label className={labelClass} htmlFor="confirm_password">
تکرار رمز عبور جدید
</label>
<input
id="confirm_password"
type="password"
dir="ltr"
className={inputClass}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
autoComplete="new-password"
/>
</div>
</div>
{error && (
<p className="mt-4 rounded-lg bg-red-50 px-3 py-2 text-sm text-red-600">
{error}
</p>
)}
{success && (
<p className="mt-4 rounded-lg bg-green-50 px-3 py-2 text-sm text-green-700">
{success}
</p>
)}
<div className="mt-6">
<Button type="submit" loading={saving} loadingText="در حال ذخیره...">
بهروزرسانی رمز عبور
</Button>
</div>
</form>
);
}