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

لندینگِ اپِ تقویم

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

لندینگِ روشن و بازیگوش: تیتر با کاشی‌های آیکونِ رنگی وسطِ جمله، پیش‌نمایشِ بزرگِ هفته با رویدادهای رنگی و خطِ اکنون، ویژگی‌ها با پیش‌نمایش، شبکهٔ امکانات، پلنِ یکتا با سوییچِ سالانه، پرسش‌ها و فراخوان.

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

این بلاک از ۶ آیتم دیگ استفاده می‌کند: accordionbuttoncontrol-sizeiconsrevealtoggle-group

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

۲. کد صفحه

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

landing-calendar.tsx
"use client";

import * as React from "react";

import {
  BellIcon,
  CalendarIcon,
  ChatIcon,
  CheckIcon,
  ClockIcon,
  GlobeIcon,
  MoonIcon,
  SearchIcon,
  ShareIcon,
  SparklesIcon,
  UsersIcon,
} from "@/components/icons";
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import { ControlSizeProvider } from "@/components/ui/control-size";
import { Reveal } from "@/components/ui/reveal";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { cn } from "@/lib/utils";

const DAYS = ["شنبه", "یکشنبه", "دوشنبه", "سه‌شنبه", "چهارشنبه"];
const TONES = {
  blue: "border-[var(--dig-blue-500)] bg-[var(--dig-blue-100)] text-[var(--dig-blue-900)] dark:bg-[var(--dig-blue-950)] dark:text-[var(--dig-blue-100)]",
  green:
    "border-[var(--dig-green-500)] bg-[var(--dig-green-100)] text-[var(--dig-green-900)] dark:bg-[var(--dig-green-950)] dark:text-[var(--dig-green-100)]",
  orange:
    "border-[var(--dig-orange-500)] bg-[var(--dig-orange-100)] text-[var(--dig-orange-900)] dark:bg-[var(--dig-orange-950)] dark:text-[var(--dig-orange-100)]",
  pink: "border-[var(--dig-pink-500)] bg-[var(--dig-pink-100)] text-[var(--dig-pink-900)] dark:bg-[var(--dig-pink-950)] dark:text-[var(--dig-pink-100)]",
  amber:
    "border-[var(--dig-amber-500)] bg-[var(--dig-amber-100)] text-[var(--dig-amber-900)] dark:bg-[var(--dig-amber-950)] dark:text-[var(--dig-amber-100)]",
};
const EVENTS: {
  day: number;
  start: number;
  span: number;
  title: string;
  tone: keyof typeof TONES;
}[] = [
  { day: 0, start: 0, span: 1, title: "جلسهٔ صبح", tone: "blue" },
  { day: 1, start: 1, span: 2, title: "طراحیِ صفحهٔ پرداخت", tone: "green" },
  { day: 2, start: 0, span: 1, title: "تماس با مشتری", tone: "pink" },
  { day: 3, start: 2, span: 2, title: "بازبینیِ کد", tone: "orange" },
  { day: 4, start: 1, span: 1, title: "ناهارِ تیمی", tone: "amber" },
  { day: 0, start: 3, span: 1, title: "ورزش", tone: "green" },
  { day: 2, start: 3, span: 1, title: "یادگیری", tone: "blue" },
];
const HOURS = ["۹", "۱۰", "۱۱", "۱۲"];
const MORE = [
  {
    icon: <CalendarIcon />,
    title: "همهٔ تقویم‌ها در یک جا",
    text: "کاری، شخصی و خانوادگی کنارِ هم.",
  },
  {
    icon: <SearchIcon />,
    title: "جست‌وجوی رویدادها",
    text: "هر جلسه‌ای را با یک کلمه پیدا کنید.",
  },
  {
    icon: <GlobeIcon />,
    title: "ساعتِ شهرهای دیگر",
    text: "همکارِ دبی و تورنتو را هم ببینید.",
  },
  {
    icon: <BellIcon />,
    title: "یادآوریِ به‌موقع",
    text: "پیش از جلسه، نه وسطِ آن.",
  },
  {
    icon: <MoonIcon />,
    title: "تعطیلاتِ رسمی",
    text: "تقویمِ شمسی با همهٔ تعطیلی‌ها.",
  },
  {
    icon: <ShareIcon />,
    title: "لینکِ رزرو",
    text: "دیگران فقط ساعتِ خالی را می‌بینند.",
  },
];
const FAQ = [
  {
    q: "با تقویمِ فعلی‌ام کار می‌کند؟",
    a: "بله، همگام‌سازیِ دوطرفه دارد و چیزی را جابه‌جا نمی‌کند.",
  },
  {
    q: "دورهٔ آزمایشی چطور است؟",
    a: "۱۴ روز همهٔ امکانات رایگان است؛ بعد خودتان تصمیم می‌گیرید.",
  },
  { q: "روی موبایل هم هست؟", a: "اپ برای اندروید و آی‌او‌اس با همان حساب." },
];

/**
 * لندینگِ اپِ تقویم، روشن و بازیگوش: تیترِ درشت با کاشی‌های آیکونِ رنگی وسطِ جمله، پیش‌نمایشِ
 * بزرگِ هفته با رویدادهای رنگی و خطِ «اکنون»، فهرستِ ویژگی‌ها با پیش‌نمایشِ کنارش، شبکهٔ
 * امکاناتِ کوچک، پلنِ یکتا با سوییچِ ماهانه/سالانه، پرسش‌ها و فراخوان.
 */
export default function LandingCalendarBlock() {
  const [billing, setBilling] = React.useState("year");

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="flex w-full flex-col">
        <section className="mx-auto flex w-full max-w-6xl flex-col gap-6 px-4 pt-20 sm:px-6">
          <h1 className="max-w-4xl text-4xl leading-[1.5] font-black text-balance sm:text-6xl sm:leading-[1.4]">
            <span className="whitespace-nowrap">
              تقویم{" "}
              <IconTile tone="pink">
                <CalendarIcon />
              </IconTile>
              ،
            </span>{" "}
            <span className="whitespace-nowrap">
              کارها{" "}
              <IconTile tone="green">
                <CheckIcon />
              </IconTile>
            </span>{" "}
            و{" "}
            <span className="whitespace-nowrap">
              جلسه‌ها{" "}
              <IconTile tone="blue">
                <UsersIcon />
              </IconTile>
              ،
            </span>{" "}
            همه در یک جا.
          </h1>
          <p className="max-w-xl text-lg leading-8 text-muted-foreground">
            روی تقویمِ فعلی‌تان سوار می‌شود: هفته را بچینید، با همکارانِ شهرهای
            دیگر هماهنگ شوید و زمانِ خالی را بفرستید.
          </p>
          <div className="flex flex-wrap gap-3">
            <Button>۱۴ روز رایگان</Button>
            <Button variant="outline">دریافتِ اپ</Button>
          </div>
        </section>

        <Reveal className="mx-auto mt-14 w-full max-w-6xl px-4 sm:px-6">
          <div className="overflow-hidden rounded-xl border border-border bg-card shadow-2xl">
            <div className="flex items-center justify-between gap-4 border-b border-border px-4 py-3">
              <span className="font-bold">مهر ۱۴۰۵</span>
              <span className="flex gap-1.5" aria-hidden>
                {[
                  "bg-[var(--dig-red-400)]",
                  "bg-[var(--dig-amber-400)]",
                  "bg-[var(--dig-green-400)]",
                ].map((dot) => (
                  <span key={dot} className={cn("size-3 rounded-full", dot)} />
                ))}
              </span>
            </div>
            <div className="grid grid-cols-[2.5rem_repeat(5,minmax(7rem,1fr))] overflow-x-auto text-xs sm:text-sm">
              <span />
              {DAYS.map((day, index) => (
                <span
                  key={day}
                  className={cn(
                    "border-s border-border px-2 py-2 font-semibold",
                    index === 2 &&
                      "text-[var(--dig-red-600)] dark:text-[var(--dig-red-400)]",
                  )}
                >
                  {day}
                </span>
              ))}
              {HOURS.map((hour, row) => (
                <React.Fragment key={hour}>
                  <span className="border-t border-border px-1 py-2 text-muted-foreground tabular-nums">
                    {hour}
                  </span>
                  {DAYS.map((day, col) => {
                    const event = EVENTS.find(
                      (item) => item.day === col && item.start === row,
                    );
                    const covered = EVENTS.some(
                      (item) =>
                        item.day === col &&
                        row > item.start &&
                        row < item.start + item.span,
                    );
                    return (
                      <span
                        key={day}
                        className="relative h-16 border-s border-t border-border"
                      >
                        {event && !covered && (
                          <span
                            className={cn(
                              "absolute inset-x-1 top-1 z-10 rounded-md border-s-4 p-1.5 font-semibold",
                              TONES[event.tone],
                            )}
                            style={{
                              height: `calc(${event.span * 4}rem - 0.5rem)`,
                            }}
                          >
                            {event.title}
                          </span>
                        )}
                        {col === 2 && row === 1 && (
                          <span
                            aria-hidden
                            className="absolute inset-x-0 top-1/2 z-20 h-0.5 bg-[var(--dig-red-500)] before:absolute before:-start-1 before:-top-1 before:size-2.5 before:rounded-full before:bg-[var(--dig-red-500)]"
                          />
                        )}
                      </span>
                    );
                  })}
                </React.Fragment>
              ))}
            </div>
          </div>
        </Reveal>

        <section
          aria-labelledby="lc-plan"
          className="mx-auto grid w-full max-w-6xl grid-cols-1 items-center gap-10 px-4 py-24 sm:px-6 lg:grid-cols-[1fr_1.3fr]"
        >
          <div className="flex flex-col gap-6">
            <span className="text-xs font-bold text-[var(--dig-orange-700)] dark:text-[var(--dig-orange-400)]">
              برنامهٔ هفته
            </span>
            <h2 id="lc-plan" className="text-3xl font-black sm:text-4xl">
              هفته را در یک نگاه بچینید
            </h2>
            <ol className="flex flex-col gap-1">
              {[
                [
                  "رویداد را بنویسید",
                  "«ناهار با سارا فردا ساعت ۱» خودش جا می‌افتد.",
                ],
                ["کارها کنارِ تقویم", "کارها را روی روزِ خالی بکشید."],
                ["منوی سریع", "هر کاری با یک میان‌بُر."],
              ].map(([title, text], index) => (
                <li
                  key={title}
                  className={cn(
                    "flex gap-4 rounded-xl p-4",
                    index === 0 && "bg-subtle",
                  )}
                >
                  <span
                    aria-hidden
                    className={cn(
                      "mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg",
                      index === 0
                        ? "bg-foreground text-background"
                        : "bg-muted",
                    )}
                  >
                    {
                      [
                        <ChatIcon key="a" className="size-4" />,
                        <CheckIcon key="b" className="size-4" />,
                        <SparklesIcon key="c" className="size-4" />,
                      ][index]
                    }
                  </span>
                  <span className="flex flex-col gap-1">
                    <span className="font-bold">{title}</span>
                    {index === 0 && (
                      <span className="text-sm leading-6 text-muted-foreground">
                        {text}
                      </span>
                    )}
                  </span>
                </li>
              ))}
            </ol>
          </div>
          <div className="rounded-xl bg-[var(--dig-orange-100)] p-6 sm:p-10 dark:bg-[var(--dig-orange-950)]">
            <div className="flex flex-col gap-3 rounded-xl bg-card p-4 shadow-lg">
              <div className="flex items-center gap-2 rounded-lg border border-border px-3 py-2 text-sm">
                <SparklesIcon
                  aria-hidden
                  className="size-4 text-muted-foreground"
                />
                ناهار با سارا{" "}
                <span className="rounded bg-[var(--dig-blue-100)] px-1 text-[var(--dig-blue-800)] dark:bg-[var(--dig-blue-950)] dark:text-[var(--dig-blue-200)]">
                  فردا
                </span>{" "}
                ساعت ۱
              </div>
              <div className="grid grid-cols-3 gap-2 text-xs">
                {DAYS.slice(0, 3).map((day, index) => (
                  <div key={day} className="flex flex-col gap-1.5">
                    <span className="text-muted-foreground">{day}</span>
                    <span className="h-10 rounded-md bg-muted" />
                    {index === 1 ? (
                      <span
                        className={cn(
                          "rounded-md border-s-4 p-1.5 font-semibold",
                          TONES.blue,
                        )}
                      >
                        ناهار با سارا
                      </span>
                    ) : (
                      <span className="h-8 rounded-md bg-muted" />
                    )}
                  </div>
                ))}
              </div>
            </div>
          </div>
        </section>

        <section
          aria-labelledby="lc-more"
          className="mx-auto flex w-full max-w-6xl flex-col gap-10 px-4 py-16 sm:px-6"
        >
          <h2
            id="lc-more"
            className="text-center text-3xl font-black sm:text-4xl"
          >
            امکاناتی که هر روز سراغشان می‌روید
          </h2>
          <ul className="grid grid-cols-1 gap-x-10 gap-y-8 sm:grid-cols-2 lg:grid-cols-3">
            {MORE.map((item, index) => (
              <Reveal asChild key={item.title} delay={(index % 3) * 80}>
                <li className="flex gap-4">
                  <span
                    aria-hidden
                    className={cn(
                      "flex size-11 shrink-0 items-center justify-center rounded-xl border-s-4 [&>svg]:size-5",
                      Object.values(TONES)[index % 5],
                    )}
                  >
                    {item.icon}
                  </span>
                  <span className="flex flex-col gap-1">
                    <span className="font-bold">{item.title}</span>
                    <span className="text-sm leading-6 text-muted-foreground">
                      {item.text}
                    </span>
                  </span>
                </li>
              </Reveal>
            ))}
          </ul>
        </section>

        <section
          aria-labelledby="lc-price"
          className="mx-auto flex w-full max-w-4xl flex-col items-center gap-8 px-4 py-20 sm:px-6"
        >
          <div className="flex flex-col items-center gap-3 text-center">
            <h2 id="lc-price" className="text-3xl font-black sm:text-4xl">
              یک پلن، همهٔ امکانات
            </h2>
            <ToggleGroup
              type="single"
              variant="segmented"
              value={billing}
              onValueChange={(value) => value && setBilling(value as string)}
              aria-label="دورهٔ پرداخت"
            >
              <ToggleGroupItem value="month">ماهانه</ToggleGroupItem>
              <ToggleGroupItem value="year">سالانه، ٪۲۰ کمتر</ToggleGroupItem>
            </ToggleGroup>
          </div>
          <div className="grid w-full grid-cols-1 overflow-hidden rounded-xl border border-border md:grid-cols-2">
            <div className="flex flex-col justify-between gap-8 bg-[var(--dig-amber-100)] p-8 dark:bg-[var(--dig-amber-950)]">
              <div className="flex flex-col gap-2">
                <span className="text-sm font-semibold">پلنِ کامل</span>
                <span className="flex items-baseline gap-2">
                  <span className="text-5xl font-black tabular-nums">
                    {billing === "year" ? "۹۶" : "۱۲۰"}
                  </span>
                  <span className="text-muted-foreground">
                    هزار تومان در ماه
                  </span>
                </span>
                <span className="text-sm text-muted-foreground">
                  {billing === "year"
                    ? "سالانه ۱٬۱۵۲ هزار تومان"
                    : "هر ماه، هر وقت خواستید لغو کنید"}
                </span>
              </div>
              <Button fullWidth>شروعِ ۱۴ روزِ رایگان</Button>
            </div>
            <ul className="flex flex-col gap-3 bg-card p-8">
              {[
                "تقویم‌ها و کارهای نامحدود",
                "لینکِ رزرو و صفحهٔ نوبت‌دهی",
                "ساعتِ شهرهای دیگر",
                "دستیارِ هوشمندِ برنامه‌ریزی",
                "اپِ موبایل و دسکتاپ",
              ].map((feature) => (
                <li key={feature} className="flex items-center gap-3 text-sm">
                  <span
                    aria-hidden
                    className="flex size-5 items-center justify-center rounded-full bg-success-subtle text-success-subtle-foreground"
                  >
                    <CheckIcon className="size-3.5" />
                  </span>
                  {feature}
                </li>
              ))}
            </ul>
          </div>
        </section>

        <section
          aria-labelledby="lc-faq"
          className="mx-auto grid w-full max-w-6xl grid-cols-1 gap-10 px-4 py-16 sm:px-6 lg:grid-cols-[1fr_1.6fr]"
        >
          <div className="flex flex-col gap-3">
            <h2 id="lc-faq" className="text-3xl font-black">
              پرسش‌های پرتکرار
            </h2>
            <p className="text-muted-foreground">
              جوابتان نبود؟ برایمان بنویسید.
            </p>
            <ClockIcon
              aria-hidden
              className="mt-4 size-16 text-[var(--dig-violet-400)]"
            />
          </div>
          <Accordion type="single" collapsible>
            {FAQ.map((item, index) => (
              <AccordionItem key={item.q} value={String(index)}>
                <AccordionTrigger>{item.q}</AccordionTrigger>
                <AccordionContent>{item.a}</AccordionContent>
              </AccordionItem>
            ))}
          </Accordion>
        </section>

        <section className="flex flex-col items-center gap-5 px-4 py-24 text-center sm:px-6">
          <h2 className="max-w-xl text-3xl leading-snug font-black text-balance sm:text-5xl sm:leading-tight">
            از همین هفته برنامه‌ریزی را شروع کنید
          </h2>
          <p className="text-muted-foreground">
            با حسابِ کاری وارد شوید؛ همه‌چیز ۱۴ روز رایگان است.
          </p>
          <div className="flex flex-wrap justify-center gap-3">
            <Button>شروعِ رایگان</Button>
            <Button variant="outline">دریافتِ اپ</Button>
          </div>
        </section>
      </main>
    </ControlSizeProvider>
  );
}

function IconTile({
  tone,
  children,
}: {
  tone: keyof typeof TONES;
  children: React.ReactNode;
}) {
  return (
    <span
      aria-hidden
      className={cn(
        "mx-1 inline-flex size-[0.95em] -rotate-6 items-center justify-center rounded-[0.22em] border-2 align-middle [&>svg]:size-[0.55em]",
        TONES[tone],
      )}
    >
      {children}
    </span>
  );
}