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

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

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

این بلاک از ۷ آیتم دیگ استفاده می‌کند: avatarbackground-patternbuttoncontrol-sizeiconsinputpersian

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/background-pattern.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/input.json https://docs.digdesign.ir/r/persian.json
pnpm dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/background-pattern.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/input.json https://docs.digdesign.ir/r/persian.json
yarn dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/background-pattern.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/input.json https://docs.digdesign.ir/r/persian.json
bunx --bun digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/background-pattern.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/input.json https://docs.digdesign.ir/r/persian.json

۲. کد صفحه

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

coming-soon.tsx
"use client"

import * as React from "react"

import { CheckIcon, EnvelopeIcon, SparklesIcon } from "@/components/icons"
import { Avatar, AvatarGroup } from "@/components/ui/avatar"
import { DotPattern } from "@/components/ui/background-pattern"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Input } from "@/components/ui/input"
import { isValidEmail, toFaDigits } from "@/lib/persian"
import { stockImages } from "@/lib/stock-images"

/** زمانِ باقی‌مانده تا رونمایی، نسبت به لحظهٔ بازکردنِ صفحه (۱۲ روز). */
const LAUNCH_IN_MS = 12 * 24 * 60 * 60 * 1000 + 7 * 60 * 60 * 1000

function useCountdown(ms: number) {
  // زمان فقط بعد از mount خوانده می‌شود تا خروجی سرور و مرورگر یکی بماند.
  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 {
    days: Math.floor(value / 86_400_000),
    hours: Math.floor(value / 3_600_000) % 24,
    minutes: Math.floor(value / 60_000) % 60,
    seconds: Math.floor(value / 1000) % 60,
  }
}

/**
 * به‌زودی و فهرستِ انتظار: هیروی تیرهٔ تمام‌صفحه با پترنِ نقطه‌ای، شمارشِ معکوسِ
 * رونمایی، فرمِ ایمیل با پیامِ موفقیت در همان جا، و اثباتِ اجتماعی با عکسِ کاربران.
 */
export default function ComingSoonBlock() {
  const { days, hours, minutes, seconds } = useCountdown(LAUNCH_IN_MS)
  const [email, setEmail] = React.useState("")
  const [joined, setJoined] = React.useState(false)

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="relative flex min-h-svh flex-col items-center justify-center overflow-hidden bg-inverse px-4 py-16 text-[var(--dig-white)] [--background:var(--inverse)] [--border:var(--inverse-border)] [--foreground:var(--dig-white)] [--muted-foreground:var(--dig-neutral-400)] [--primary-foreground:var(--dig-graphite-950)] [--primary:var(--dig-white)]">
        <DotPattern size={22} />
        <span aria-hidden className="absolute -top-40 size-[36rem] rounded-full bg-[var(--dig-violet-800)] blur-3xl" />
        <div className="relative flex w-full max-w-2xl flex-col items-center gap-10 text-center">
          <span className="flex items-center gap-2 font-black">
            <span aria-hidden className="flex size-9 items-center justify-center rounded-lg bg-[var(--dig-violet-500)] [&>svg]:size-4">
              <SparklesIcon />
            </span>
            حساب‌یار ۳
          </span>
          <div className="flex flex-col gap-4">
            <h1 className="text-4xl leading-snug font-black sm:text-6xl sm:leading-snug">چیزِ بزرگی در راه است</h1>
            <p className="text-lg leading-8 text-muted-foreground">نسخهٔ تازه با دستیارِ هوشمندِ مالی. زودتر از همه امتحانش کن و سه ماه رایگان بگیر.</p>
          </div>

          {/* شمارشِ معکوس: چهار کارتِ هم‌اندازه. */}
          <div className="grid w-full max-w-md grid-cols-4 gap-3" role="timer" aria-label={`${toFaDigits(days)} روز و ${toFaDigits(hours)} ساعت تا رونمایی`}>
            {[
              { value: days, label: "روز" },
              { value: hours, label: "ساعت" },
              { value: minutes, label: "دقیقه" },
              { value: seconds, label: "ثانیه" },
            ].map((unit) => (
              <div key={unit.label} className="flex flex-col items-center gap-1 rounded-xl border border-inverse-border bg-inverse-raised py-4">
                <span className="text-3xl font-black tabular-nums">{toFaDigits(String(unit.value).padStart(2, "0"))}</span>
                <span className="text-xs text-muted-foreground">{unit.label}</span>
              </div>
            ))}
          </div>

          {joined ? (
            <p className="flex items-center gap-2 rounded-xl bg-inverse-raised px-5 py-4" role="status">
              <CheckIcon aria-hidden className="size-5 text-[var(--dig-green-400)]" />
              توی فهرست هستی! روزِ رونمایی اولین نفر خبرت می‌کنیم.
            </p>
          ) : (
            <form
              onSubmit={(event) => {
                event.preventDefault()
                if (isValidEmail(email)) setJoined(true)
              }}
              className="flex w-full max-w-md flex-col gap-3 sm:flex-row"
            >
              <Input
                type="email"
                dir="ltr"
                aria-label="ایمیل"
                placeholder="name@example.com"
                startContent={<EnvelopeIcon />}
                value={email}
                onChange={(event) => setEmail(event.target.value)}
                className="flex-1 bg-[var(--dig-white)] text-[var(--dig-graphite-900)]"
              />
              <Button type="submit" disabled={!isValidEmail(email)}>
                عضویت در فهرستِ انتظار
              </Button>
            </form>
          )}

          <div className="flex items-center gap-3 text-sm text-muted-foreground">
            <AvatarGroup>
              {(["personWoman1", "personMan1", "personWoman3", "personMan2"] as const).map((name) => (
                <Avatar key={name} size="sm" alt="" src={stockImages[name].src} className="ring-2 ring-inverse" />
              ))}
            </AvatarGroup>
            {toFaDigits("۲٬۳۸۰")} نفر قبل از تو ثبت‌نام کرده‌اند
          </div>
        </div>
      </main>
    </ControlSizeProvider>
  )
}