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