پروفایل
پیشنمایش تمامصفحهاطلاعات کاربر با آواتار، کد ملی، ایمیل، موبایل، تاریخ تولد شمسی و شهر.
۱. نصب کامپوننتها
این بلاک از ۱۴ آیتم دیگ استفاده میکند: autocompleteavatarbuttoncardcontrol-sizedate-pickericonsiran-locationslabelpersianprogresstext-fieldtoasttoggle-group
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/autocomplete.json https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/date-picker.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/iran-locations.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/autocomplete.json https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/date-picker.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/iran-locations.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/autocomplete.json https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/date-picker.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/iran-locations.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/autocomplete.json https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/date-picker.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/iran-locations.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
profile.tsx
"use client"
import * as React from "react"
import { CheckIcon, ImageIcon } from "@/components/icons"
import { Autocomplete } from "@/components/ui/autocomplete"
import { Avatar } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { DatePicker } from "@/components/ui/date-picker"
import { Label } from "@/components/ui/label"
import { Progress } from "@/components/ui/progress"
import { TextField } from "@/components/ui/text-field"
import { toast } from "@/components/ui/toast"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { IRAN_PROVINCES } from "@/lib/iran-locations"
import { isValidEmail, isValidMobile, isValidNationalId, toFaDigits } from "@/lib/persian"
import { stockImages } from "@/lib/stock-images"
// همهٔ شهرها با نامِ استان، تا «آباد»های همنام از هم جدا شوند.
const CITY_ITEMS = IRAN_PROVINCES.flatMap((province) =>
province.cities.map((city) => ({ value: `${province.name}/${city}`, label: `${city}، ${province.name}` }))
)
const CHECKLIST = [
{ label: "عکسِ پروفایل", done: true },
{ label: "تأییدِ موبایل", done: true },
{ label: "کدِ ملی", done: false },
{ label: "تاریخِ تولد", done: false },
]
/**
* پروفایل: سربرگِ رنگی با عکسِ کاربر روی لبه و تغییرِ عکس، کارتهای اطلاعاتِ شخصی و
* تماس، و ستونِ کنارِ «کاملبودنِ پروفایل» با چکلیست. ذخیره در پایانِ فرم.
*/
export default function ProfileBlock() {
const [saving, setSaving] = React.useState(false)
const [gender, setGender] = React.useState("female")
const done = CHECKLIST.filter((item) => item.done).length
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
setSaving(true)
await new Promise((resolve) => setTimeout(resolve, 700))
setSaving(false)
toast.success("پروفایل ذخیره شد.")
}
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-5xl flex-col gap-8 p-4 sm:p-6">
{/* سربرگ: نوارِ برند و عکسِ کاربر که روی لبهاش مینشیند. */}
<section className="overflow-hidden rounded-xl border border-border">
<div aria-hidden className="h-32 bg-linear-to-l from-[var(--dig-violet-700)] to-[var(--dig-violet-500)]" />
<div className="flex flex-wrap items-end justify-between gap-4 px-6 pb-6">
<div className="flex items-start gap-4">
<Avatar alt="مهسا کریمی" src={stockImages.personWoman1.src} className="-mt-12 size-24 shrink-0 ring-4 ring-card" />
<div className="flex flex-col gap-0.5 pt-3">
<h1 className="text-2xl font-bold">مهسا کریمی</h1>
<span className="text-sm text-muted-foreground">طراحِ ارشدِ محصول · عضو از ۱۴۰۲</span>
</div>
</div>
<Button variant="outline">
<ImageIcon />
تغییرِ عکس
</Button>
</div>
</section>
<form onSubmit={onSubmit} className="grid grid-cols-1 items-start gap-6 lg:grid-cols-[minmax(0,1fr)_18rem]">
<div className="flex flex-col gap-6">
<Card>
<CardHeader>
<CardTitle className="text-base">اطلاعاتِ شخصی</CardTitle>
<CardDescription>همانطور که در کارتِ ملی آمده.</CardDescription>
</CardHeader>
<CardContent className="grid grid-cols-1 gap-5 sm:grid-cols-2">
<TextField label="نام" name="firstName" defaultValue="مهسا" autoComplete="given-name" required />
<TextField label="نام خانوادگی" name="lastName" defaultValue="کریمی" autoComplete="family-name" required />
<TextField
label="کد ملی"
name="nationalId"
inputMode="numeric"
dir="ltr"
maxLength={10}
validate={(value) => (!value || isValidNationalId(value) ? undefined : value.length === 10 ? "کد ملی معتبر نیست." : "کد ملی باید ۱۰ رقم باشد.")}
/>
<DatePicker id="birth" label="تاریخ تولد" calendar="jalali" maxDate={new Date()} className="w-full" />
<div className="flex flex-col gap-2 sm:col-span-2">
<span id="profile-gender" className="text-sm font-medium">
جنسیت
</span>
<ToggleGroup type="single" variant="segmented" value={gender} onValueChange={(value) => value && setGender(value as string)} aria-labelledby="profile-gender" className="self-start">
<ToggleGroupItem value="female">زن</ToggleGroupItem>
<ToggleGroupItem value="male">مرد</ToggleGroupItem>
</ToggleGroup>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">تماس</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 gap-5 sm:grid-cols-2">
<TextField label="ایمیل" name="email" type="email" dir="ltr" defaultValue="mahsa@dig.example" autoComplete="email" validate={(value) => (isValidEmail(value) ? undefined : "ایمیل معتبر نیست؛ مثل name@example.com")} />
<TextField label="شماره موبایل" name="mobile" type="tel" dir="ltr" maxLength={11} defaultValue="09121234567" validate={(value) => (isValidMobile(value) ? undefined : "شماره موبایل باید با ۰۹ شروع شود و ۱۱ رقم باشد.")} />
<div className="flex flex-col gap-2 sm:col-span-2">
<Label htmlFor="city">شهر</Label>
<Autocomplete id="city" items={CITY_ITEMS} defaultValue="تهران/تهران" emptyText="شهری پیدا نشد" className="w-full" />
</div>
</CardContent>
</Card>
<div className="flex justify-end gap-3 border-t border-border pt-8">
<Button type="reset" variant="outline">
انصراف
</Button>
<Button type="submit" loading={saving}>
ذخیرهٔ تغییرات
</Button>
</div>
</div>
<Card className="lg:sticky lg:top-6">
<CardHeader>
<CardTitle className="text-base">کاملبودنِ پروفایل</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-5">
<div className="flex flex-col gap-2">
<span className="text-3xl font-black tabular-nums">{toFaDigits(Math.round((done / CHECKLIST.length) * 100))}٪</span>
<Progress value={(done / CHECKLIST.length) * 100} aria-label="کاملبودنِ پروفایل" />
</div>
<ul className="flex flex-col gap-3 text-sm">
{CHECKLIST.map((item) => (
<li key={item.label} className="flex items-center gap-2">
<span
aria-hidden
className={`flex size-5 items-center justify-center rounded-full [&>svg]:size-3 ${item.done ? "bg-success text-[var(--dig-white)]" : "border border-input"}`}
>
{item.done && <CheckIcon />}
</span>
<span className={item.done ? "text-muted-foreground line-through" : ""}>{item.label}</span>
</li>
))}
</ul>
</CardContent>
</Card>
</form>
</main>
</ControlSizeProvider>
)
}