feat: refine edit profile page
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Camera, Check, CircleAlert, Eye, Loader2, LockKeyhole, Trash2, UserRound } from "lucide-react";
|
||||
import { Camera, Check, CircleAlert, Loader2, Trash2 } from "lucide-react";
|
||||
import AsyncSearchableCombobox from "@/components/AsyncSearchableCombobox";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -41,13 +41,6 @@ const emptyForm: EditForm = {
|
||||
show_public_profile_details: false,
|
||||
};
|
||||
|
||||
const editorSections = [
|
||||
{ id: "identity", label: "اطلاعات فردی", icon: UserRound },
|
||||
{ id: "academic", label: "اطلاعات دانشگاهی", icon: LockKeyhole },
|
||||
{ id: "about", label: "درباره من", icon: Camera },
|
||||
{ id: "visibility", label: "پروفایل عمومی", icon: Eye },
|
||||
] as const;
|
||||
|
||||
export default function ProfileEdit() {
|
||||
const { isAuthenticated, loading, setUser } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -261,7 +254,7 @@ export default function ProfileEdit() {
|
||||
<main className="min-h-[70vh] w-full min-w-0 overflow-x-clip bg-muted/15 px-4 py-6 md:py-12" dir="rtl">
|
||||
<div className="container mx-auto w-full min-w-0 max-w-5xl">
|
||||
<div className="mb-6 min-w-0 text-right">
|
||||
<p className="text-sm text-muted-foreground">حساب کاربری / پروفایل</p>
|
||||
<p className="text-sm text-muted-foreground">حساب کاربری</p>
|
||||
<h1 className="mt-1 text-2xl font-bold md:text-3xl">ویرایش پروفایل</h1>
|
||||
</div>
|
||||
|
||||
@@ -291,20 +284,18 @@ export default function ProfileEdit() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid min-w-0 gap-5 lg:grid-cols-[minmax(0,220px)_minmax(0,1fr)]">
|
||||
<nav className="hidden min-w-0 self-start lg:sticky lg:top-24 lg:flex lg:flex-col lg:gap-2 lg:rounded-2xl lg:border lg:border-border/70 lg:bg-card lg:p-2" aria-label="بخشهای ویرایش پروفایل">
|
||||
{editorSections.map(({ id, label, icon: Icon }) => (
|
||||
<a key={id} href={`#${id}`} className="inline-flex min-h-11 min-w-0 items-center justify-center gap-2 rounded-xl px-2 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:justify-start sm:px-3 sm:text-sm">
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />{label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="min-w-0 space-y-5">
|
||||
<div className="min-w-0 space-y-5">
|
||||
<Card id="identity" className="scroll-mt-24 min-w-0 border-border/70 shadow-sm">
|
||||
<CardHeader className="text-right">
|
||||
<CardTitle>اطلاعات فردی</CardTitle>
|
||||
<CardDescription>نامی که در سایت و پروفایل عمومی شما نمایش داده میشود</CardDescription>
|
||||
<CardTitle className="flex items-center gap-3">
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-muted/70">
|
||||
<svg viewBox="0 0 24 24" className="h-5 w-5 fill-current text-foreground" aria-hidden="true">
|
||||
<path d="M12 12a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9Zm0 2c-4.42 0-8 2.24-8 5v2h16v-2c0-2.76-3.58-5-8-5Z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span>اطلاعات فردی</span>
|
||||
</CardTitle>
|
||||
<CardDescription>این اطلاعات در پروفایل عمومی شما نمایش داده میشوند.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid min-w-0 gap-4 sm:grid-cols-2">
|
||||
<div className="min-w-0"><Label htmlFor="first_name">نام</Label><Input id="first_name" autoComplete="given-name" value={form.first_name ?? ""} onChange={(event) => updateForm("first_name", event.target.value)} className="mt-2 w-full min-w-0 text-right" /></div>
|
||||
@@ -339,13 +330,35 @@ export default function ProfileEdit() {
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="min-w-0 sm:col-span-2">
|
||||
<Label htmlFor="bio">معرفی کوتاه</Label>
|
||||
<Textarea
|
||||
id="bio"
|
||||
value={form.bio ?? ""}
|
||||
onChange={(event) => updateForm("bio", event.target.value)}
|
||||
className={`mt-2 min-h-28 resize-y text-right leading-7 ${bioHasError ? "border-destructive focus-visible:ring-destructive" : ""}`}
|
||||
maxLength={BIO_MAX_LENGTH}
|
||||
aria-invalid={bioHasError}
|
||||
aria-describedby="bio-count"
|
||||
/>
|
||||
<p id="bio-count" aria-live="polite" className={`mt-2 text-xs ${bioHasError ? "text-destructive" : "text-muted-foreground"}`}>
|
||||
{bioLengthLabel} از {bioMaxLengthLabel} نویسه{bioHasError ? " — برای ذخیره، متن را کوتاه کنید." : ""}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card id="academic" className="scroll-mt-24 min-w-0 border-border/70 shadow-sm">
|
||||
<CardHeader className="text-right">
|
||||
<CardTitle>اطلاعات دانشگاهی</CardTitle>
|
||||
<CardDescription>با فعالکردن نمایش جزئیات پروفایل عمومی، این اطلاعات برای همه قابل مشاهده میشوند.</CardDescription>
|
||||
<CardTitle className="flex items-center gap-3">
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-muted/70">
|
||||
<svg viewBox="0 0 24 24" className="h-5 w-5 fill-current text-foreground" aria-hidden="true">
|
||||
<path d="M12 3 1 9l11 6 9-4.91V17h2V9L12 3Zm-6 9.18V17l6 4 6-4v-4.82l-6 3.27-6-3.27Z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span>اطلاعات دانشگاهی</span>
|
||||
</CardTitle>
|
||||
<CardDescription className="leading-7">تا زمانی که پروفایل خود را به صورت خصوصی نگهدارید این اطلاعات به سایر کاربران نمایش داده نمیشود و تنها زمانی که پروفایل خود را به صورت عمومی منتشر کنید، این اطلاعات برای سایر کاربران قابل مشاهده میشوند.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid min-w-0 gap-4 sm:grid-cols-2">
|
||||
<div className="min-w-0"><Label htmlFor="university">دانشگاه</Label><div className="mt-2 min-w-0"><AsyncSearchableCombobox id="university" value={form.university || null} selectedLabel={profile.university} onChange={(value) => updateForm("university", value)} loadOptions={loadUniversities} placeholder="انتخاب دانشگاه" /></div></div>
|
||||
@@ -375,27 +388,20 @@ export default function ProfileEdit() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card id="about" className="scroll-mt-24 min-w-0 border-border/70 shadow-sm">
|
||||
<CardHeader className="text-right"><CardTitle>درباره من</CardTitle><CardDescription>یک معرفی کوتاه و ساده بنویسید. متن در پروفایل عمومی بدون قالببندی نمایش داده میشود.</CardDescription></CardHeader>
|
||||
<CardContent>
|
||||
<Label htmlFor="bio">معرفی کوتاه</Label>
|
||||
<Textarea
|
||||
id="bio"
|
||||
value={form.bio ?? ""}
|
||||
onChange={(event) => updateForm("bio", event.target.value)}
|
||||
className={`mt-2 min-h-28 resize-y text-right leading-7 ${bioHasError ? "border-destructive focus-visible:ring-destructive" : ""}`}
|
||||
maxLength={BIO_MAX_LENGTH}
|
||||
aria-invalid={bioHasError}
|
||||
aria-describedby="bio-count"
|
||||
/>
|
||||
<p id="bio-count" aria-live="polite" className={`mt-2 text-xs ${bioHasError ? "text-destructive" : "text-muted-foreground"}`}>
|
||||
{bioLengthLabel} از {bioMaxLengthLabel} نویسه{bioHasError ? " — برای ذخیره، متن را کوتاه کنید." : ""}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card id="visibility" className="scroll-mt-24 min-w-0 border-border/70 shadow-sm">
|
||||
<CardHeader className="text-right"><CardTitle>پروفایل عمومی</CardTitle><CardDescription>پروفایل عمومی رویدادهایی را که در آنها شرکت کردهاید نشان میدهد. این گزینه، جزئیات پروفایل را هم در اختیار بازدیدکنندگان قرار میدهد.</CardDescription></CardHeader>
|
||||
<CardHeader className="text-right">
|
||||
<CardTitle className="flex items-center gap-3">
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-muted/70">
|
||||
<svg viewBox="0 0 24 24" className="h-5 w-5 text-foreground" aria-hidden="true">
|
||||
<path className="fill-current" d="M12 5c-5.2 0-9.38 3.17-11 7 1.62 3.83 5.8 7 11 7s9.38-3.17 11-7c-1.62-3.83-5.8-7-11-7Z" />
|
||||
<circle cx="12" cy="12" r="3.3" className="fill-[hsl(var(--muted))]" />
|
||||
<circle cx="12" cy="12" r="1.6" className="fill-current" />
|
||||
</svg>
|
||||
</span>
|
||||
<span>پروفایل عمومی</span>
|
||||
</CardTitle>
|
||||
<CardDescription className="leading-7">پروفایل عمومی خود را میتوانید از طریق <Link to={`/users/profiles/${encodeURIComponent(profile.username)}`} target="_blank" rel="noopener noreferrer" className="font-medium text-primary underline-offset-4 hover:underline">این لینک</Link> مشاهده کنید. شما میتوانید اطلاعات حساس خود را از دید سایر کاربران مخفی کنید.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-5">
|
||||
<div className="flex min-w-0 items-start justify-between gap-4 rounded-2xl border border-border/70 p-4">
|
||||
<div className="min-w-0 flex-1 text-right">
|
||||
@@ -403,13 +409,13 @@ export default function ProfileEdit() {
|
||||
<Label htmlFor="show-public-details" className="cursor-pointer text-sm font-semibold">نمایش جزئیات پروفایل برای همه</Label>
|
||||
<span aria-live="polite" className={`rounded-full px-2 py-0.5 text-xs font-semibold ${form.show_public_profile_details ? "bg-primary/15 text-primary" : "bg-muted text-muted-foreground"}`}>{form.show_public_profile_details ? "فعال" : "غیرفعال"}</span>
|
||||
</div>
|
||||
<p id="public-details-description" className="mt-1 max-w-xl text-sm leading-6 text-muted-foreground">با فعالکردن این گزینه، شماره موبایل، شماره دانشجویی، دانشگاه، رشته و سال ورود در پروفایل عمومی دیده میشوند.</p>
|
||||
<p id="public-details-description" className="mt-1 text-sm leading-7 text-muted-foreground">با فعالکردن این گزینه، شماره موبایل، شماره دانشجویی، دانشگاه، رشته و سال ورود در پروفایل عمومی دیده میشوند.</p>
|
||||
</div>
|
||||
<Switch id="show-public-details" checked={Boolean(form.show_public_profile_details)} onCheckedChange={(checked) => updateForm("show_public_profile_details", checked)} aria-label="نمایش جزئیات پروفایل برای همه" aria-describedby="public-details-description" className="shrink-0 data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted-foreground/40" />
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div><Label htmlFor="mobile">شماره موبایل</Label><Input id="mobile" value={profile.mobile ? toPersianDigits(profile.mobile) : "ثبت نشده"} readOnly aria-readonly="true" className="mt-2 text-right read-only:cursor-not-allowed read-only:bg-muted/40" /><p className="mt-1 text-xs text-muted-foreground">تغییر شماره موبایل از این صفحه امکانپذیر نیست. برای بهروزرسانی آن، روند تأیید شماره را دنبال کنید.</p></div>
|
||||
<div><Label htmlFor="email">ایمیل</Label><Input id="email" value={profile.email ?? "ثبت نشده"} readOnly aria-readonly="true" className="mt-2 text-right read-only:cursor-not-allowed read-only:bg-muted/40" /><p className="mt-1 text-xs text-muted-foreground">تغییر ایمیل از این صفحه امکانپذیر نیست.</p></div>
|
||||
<div><Label htmlFor="mobile">شماره موبایل</Label><Input id="mobile" value={profile.mobile ? toPersianDigits(profile.mobile) : "ثبت نشده"} readOnly aria-readonly="true" className="mt-2 text-right read-only:cursor-not-allowed read-only:bg-muted/40" /><p className="mt-1 text-xs text-muted-foreground">تغییر شماره موبایل امکانپذیر نیست</p></div>
|
||||
<div><Label htmlFor="email">ایمیل</Label><Input id="email" value={profile.email ?? "ثبت نشده"} readOnly aria-readonly="true" className="mt-2 text-right read-only:cursor-not-allowed read-only:bg-muted/40" /><p className="mt-1 text-xs text-muted-foreground">تغییر ایمیل امکانپذیر نیست.</p></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -421,7 +427,6 @@ export default function ProfileEdit() {
|
||||
{saving ? "در حال ذخیره..." : "ذخیره تغییرات"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user