وبینار و رویداد
پیشنمایش تمامصفحهصفحهٔ رویداد: هیروی تیره با تاریخ، ساعت، محل و شمارشِ معکوس، فرمِ ثبتنامِ کوتاه با ظرفیتِ باقیمانده و پیامِ موفقیتِ درجا، سخنرانها با عکس و برنامهٔ زمانی.
۱. نصب کامپوننتها
این بلاک از ۷ آیتم دیگ استفاده میکند: 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.jsonpnpm 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.jsonyarn 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.jsonbunx --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>
)
}