بلاک‌ها · حساب کاربری

اطلاعات کاربر با آواتار، کد ملی، ایمیل، موبایل، تاریخ تولد شمسی و شهر.

۱. نصب کامپوننت‌ها

این بلاک از ۱۴ آیتم دیگ استفاده می‌کند: 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.json
pnpm 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.json
yarn 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.json
bunx --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>
  )
}