بلاک‌ها · معرفی محصول

وبینار و رویداد

پیش‌نمایش تمام‌صفحه

صفحهٔ رویداد: هیروی تیره با تاریخ، ساعت، محل و شمارشِ معکوس، فرمِ ثبت‌نامِ کوتاه با ظرفیتِ باقی‌مانده و پیامِ موفقیتِ درجا، سخنران‌ها با عکس و برنامهٔ زمانی.

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

این بلاک از ۷ آیتم دیگ استفاده می‌کند: avatarbadgebuttoncontrol-sizeiconspersiantext-field

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/text-field.json
pnpm dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/text-field.json
yarn dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/text-field.json
bunx --bun digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/text-field.json

۲. کد صفحه

کد را در app/<route>/page.tsx بگذارید و متن‌ها، داده و هندلرها را با محصول خودتان عوض کنید.

webinar.tsx
"use client"

import * as React from "react"

import { CalendarIcon, CheckIcon, ClockIcon, GlobeIcon } from "@/components/icons"
import { Avatar, AvatarGroup } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { TextField } from "@/components/ui/text-field"
import { isValidEmail, isValidMobile, toFaDigits } from "@/lib/persian"
import { stockImages, type StockImageName } from "@/lib/stock-images"

const STARTS_IN_MS = (2 * 24 * 60 + 5 * 60 + 42) * 60 * 1000
const SPEAKERS: { name: string; role: string; photo: StockImageName }[] = [
  { name: "سارا محمدی", role: "مدیرِ محصول، آوند", photo: "personWoman2" },
  { name: "امیر رضایی", role: "بنیان‌گذارِ فروشگاهِ بوم", photo: "personMan1" },
  { name: "نگار احمدی", role: "مشاورِ مالی کسب‌وکار", photo: "personWoman3" },
]
const AGENDA = [
  { time: "۱۸:۰۰", title: "چرا کسب‌وکارهای کوچک پول گم می‌کنند" },
  { time: "۱۸:۲۰", title: "سه گزارشی که هر هفته باید ببینید" },
  { time: "۱۸:۴۵", title: "روایتِ فروشگاهِ بوم: از دفترچه تا داشبورد" },
  { time: "۱۹:۱۰", title: "پرسش و پاسخِ زنده" },
]

/** زمانِ باقی‌مانده فقط بعد از mount خوانده می‌شود تا خروجی سرور و مرورگر یکی بماند. */
function useCountdown(ms: number) {
  const [left, setLeft] = React.useState<number | null>(null)
  React.useEffect(() => {
    const target = Date.now() + ms
    const tick = () => setLeft(Math.max(0, target - Date.now()))
    const first = requestAnimationFrame(tick)
    const timer = setInterval(tick, 1000)
    return () => {
      cancelAnimationFrame(first)
      clearInterval(timer)
    }
  }, [ms])
  const value = left ?? ms
  return [
    { label: "روز", value: Math.floor(value / 86_400_000) },
    { label: "ساعت", value: Math.floor(value / 3_600_000) % 24 },
    { label: "دقیقه", value: Math.floor(value / 60_000) % 60 },
    { label: "ثانیه", value: Math.floor(value / 1000) % 60 },
  ]
}

/**
 * وبینار و رویداد: هیروی تیره با تاریخ، ساعت و شمارشِ معکوس، فرمِ ثبت‌نامِ کوتاه با پیامِ
 * موفقیتِ درجا، سخنران‌ها با عکسِ واقعی و برنامهٔ زمانی. ظرفیتِ باقی‌مانده فوریت می‌سازد.
 */
export default function WebinarBlock() {
  const parts = useCountdown(STARTS_IN_MS)
  const [done, setDone] = React.useState(false)

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="flex w-full flex-col">
        <section className="bg-inverse text-[var(--dig-white)] [--border:var(--inverse-border)] [--muted-foreground:var(--dig-neutral-400)]">
          <div className="mx-auto grid grid-cols-1 w-full max-w-6xl items-start gap-12 px-4 py-16 sm:px-6 lg:grid-cols-[minmax(0,1.3fr)_minmax(0,1fr)] lg:py-24">
            <div className="flex flex-col items-start gap-6">
              <Badge className="bg-[var(--dig-red-500)] text-[var(--dig-white)]">وبینارِ رایگان · آنلاین</Badge>
              <h1 className="text-4xl leading-snug font-black text-balance sm:text-5xl sm:leading-snug">پولِ کسب‌وکارت کجا گم می‌شود؟</h1>
              <p className="max-w-xl text-lg leading-8 text-muted-foreground">یک ساعت و نیم با سه آدمِ باتجربه دربارهٔ گزارش‌هایی که جلوی ضرر را می‌گیرند؛ با پرسش و پاسخِ زنده.</p>
              <ul className="flex flex-wrap gap-x-6 gap-y-3 text-sm">
                <li className="flex items-center gap-2">
                  <CalendarIcon aria-hidden className="size-4 text-[var(--dig-orange-400)]" />
                  پنجشنبه ۱۷ مهر
                </li>
                <li className="flex items-center gap-2">
                  <ClockIcon aria-hidden className="size-4 text-[var(--dig-orange-400)]" />
                  ساعتِ ۱۸ تا ۱۹:۳۰
                </li>
                <li className="flex items-center gap-2">
                  <GlobeIcon aria-hidden className="size-4 text-[var(--dig-orange-400)]" />
                  روی اسکای‌روم
                </li>
              </ul>
              <div className="mt-4 flex gap-3" role="timer" aria-label="زمان تا شروع">
                {parts.map((part) => (
                  <div key={part.label} className="flex w-20 flex-col items-center gap-1 rounded-xl bg-inverse-raised py-3">
                    <span className="text-3xl font-black tabular-nums">{toFaDigits(String(part.value).padStart(2, "0"))}</span>
                    <span className="text-xs text-muted-foreground">{part.label}</span>
                  </div>
                ))}
              </div>
            </div>

            <div className="flex flex-col gap-6 rounded-xl bg-card p-6 text-card-foreground [--muted-foreground:var(--dig-neutral-500)] sm:p-8">
              {done ? (
                <div role="status" className="flex flex-col items-center gap-4 py-8 text-center">
                  <span aria-hidden className="flex size-14 items-center justify-center rounded-full bg-success-subtle text-success [&>svg]:size-7">
                    <CheckIcon />
                  </span>
                  <h2 className="text-xl font-bold">جایت رزرو شد</h2>
                  <p className="leading-7 text-muted-foreground">لینکِ ورود یک ساعت قبل پیامک می‌شود.</p>
                  <Button variant="outline">افزودن به تقویم</Button>
                </div>
              ) : (
                <form
                  className="flex flex-col gap-5"
                  onSubmit={(event) => {
                    event.preventDefault()
                    setDone(true)
                  }}
                >
                  <div className="flex flex-col gap-1">
                    <h2 className="text-xl font-bold">ثبت‌نامِ رایگان</h2>
                    <p className="text-sm text-destructive">فقط ۴۳ جای خالی مانده</p>
                  </div>
                  <TextField label="نام و نام خانوادگی" name="name" autoComplete="name" required />
                  <TextField label="شماره موبایل" name="mobile" type="tel" dir="ltr" maxLength={11} required validate={(value) => (isValidMobile(value) ? undefined : "شماره موبایل باید با ۰۹ شروع شود و ۱۱ رقم باشد.")} />
                  <TextField label="ایمیل" name="email" type="email" dir="ltr" validate={(value) => (!value || isValidEmail(value) ? undefined : "ایمیل معتبر نیست.")} />
                  <Button type="submit" fullWidth className="mt-3">
                    رزروِ جا
                  </Button>
                  <div className="flex items-center justify-center gap-3">
                    <AvatarGroup>
                      {[stockImages.personMan2, stockImages.personWoman1, stockImages.personMan1].map((photo) => (
                        <Avatar key={photo.src} size="sm" alt="" src={photo.src} />
                      ))}
                    </AvatarGroup>
                    <span className="text-sm text-muted-foreground">۴۵۷ نفر ثبت‌نام کرده‌اند</span>
                  </div>
                </form>
              )}
            </div>
          </div>
        </section>

        <div className="mx-auto grid grid-cols-1 w-full max-w-6xl gap-16 px-4 py-20 sm:px-6 lg:grid-cols-2">
          <section aria-labelledby="webinar-speakers" className="flex flex-col gap-6">
            <h2 id="webinar-speakers" className="text-2xl font-black">
              سخنران‌ها
            </h2>
            <ul className="flex flex-col gap-4">
              {SPEAKERS.map((speaker) => (
                <li key={speaker.name} className="flex items-center gap-4 rounded-xl bg-subtle p-4">
                  {/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
                  <img src={stockImages[speaker.photo].src} alt={speaker.name} className="size-16 rounded-lg object-cover" />
                  <span className="flex flex-col gap-0.5">
                    <span className="font-bold">{speaker.name}</span>
                    <span className="text-sm text-muted-foreground">{speaker.role}</span>
                  </span>
                </li>
              ))}
            </ul>
          </section>
          <section aria-labelledby="webinar-agenda" className="flex flex-col gap-6">
            <h2 id="webinar-agenda" className="text-2xl font-black">
              برنامه
            </h2>
            <ol className="flex flex-col">
              {AGENDA.map((item, index) => (
                <li key={item.time} className="relative flex gap-5 pb-8 last:pb-0">
                  {index < AGENDA.length - 1 && <span aria-hidden className="absolute start-[1.6rem] top-9 bottom-0 w-px bg-border" />}
                  <span className="flex h-9 w-14 shrink-0 items-center justify-center rounded-lg bg-[var(--dig-violet-50)] text-sm font-bold text-[var(--dig-violet-800)] tabular-nums dark:bg-inverse-raised dark:text-[var(--dig-violet-300)]">{item.time}</span>
                  <span className="pt-1.5 leading-7 font-medium">{item.title}</span>
                </li>
              ))}
            </ol>
          </section>
        </div>
      </main>
    </ControlSizeProvider>
  )
}