feat: split big file and update agents.md

This commit is contained in:
2026-06-18 23:17:14 +03:00
parent e383c3fdc5
commit 973f7a924d
29 changed files with 907 additions and 762 deletions

View File

@@ -5,7 +5,7 @@ Juniper Table — seasonal restaurant template for booking-driven dining: keep m
## Project Specifics ## Project Specifics
- Source content lives in \`src/entities/site-content.ts\`; keep visible copy there or directly in route JSX. - Source content lives in \`src/entities/site-content.ts\`; keep visible copy there or directly in route JSX.
- \`src/app\` is routing only; page composition belongs to \`src/widgets/template-ui.tsx\`. - \`src/app\` is routing only; композиция каждой страницы живёт в отдельном widget (\`src/widgets/<page>-page.tsx\`). См. File Map.
- Keep cards at 8px radius or less and preserve the restaurant palette in \`src/app/globals.css\`. - Keep cards at 8px radius or less and preserve the restaurant palette in \`src/app/globals.css\`.
- This is a static frontend template: do not add real payments, deposits, table-management API, delivery, auth, persistence, external API calls, or backend contracts without an explicit product request. - This is a static frontend template: do not add real payments, deposits, table-management API, delivery, auth, persistence, external API calls, or backend contracts without an explicit product request.
- Keep restaurant specificity: seating, service windows, host confirmation, allergies/dietary requests, pairing, supplier story and private dining packages. - Keep restaurant specificity: seating, service windows, host confirmation, allergies/dietary requests, pairing, supplier story and private dining packages.
@@ -15,3 +15,67 @@ Juniper Table — seasonal restaurant template for booking-driven dining: keep m
- Кириллица обязательна для видимого текста и выбранных Google Fonts. - Кириллица обязательна для видимого текста и выбранных Google Fonts.
- Do not flatten this into a generic cafe landing page. Every page should answer a real restaurant business question. - Do not flatten this into a generic cafe landing page. Every page should answer a real restaurant business question.
## Design System
Источник токенов — `src/app/globals.css` (`@theme inline` + `:root`/`.dark`). Шрифты: заголовки (`h1/h2/h3`, `.font-display`) — **Cormorant Garamond** (`--font-display`, элегантный serif), текст/UI — **Manrope** (`--font-sans`). Работай через семантические классы Tailwind (`bg-primary`, `text-foreground`, `border-border`, `text-muted-foreground`, `font-display`), никогда не хардкодь hex/oklch.
Личность: **тёплый сезонный fine-dining** — кремово-пергаментный фон (тёплый, hue ~7592), глубокий приглушённый «можжевеловый» зелёный primary (hue ~145) как фирменный цвет, тёплый терракот/обожжённый оранжевый accent (hue ~34). Контраст элегантного serif-заголовка и спокойного гротеска в тексте. Тон ресторанный, тёплый, не «технологичный».
| Роль | Light | Характер |
|---|---|---|
| `background` | тёплый кремовый пергамент | основной фон страниц |
| `foreground` | тёмный тёплый коричнево-чёрный | текст |
| `primary` | глубокий можжевеловый зелёный | бренд: лого JT, CTA, цены, акценты; footer/CTA-панели `bg-primary text-primary-foreground` |
| `accent` | терракот/обожжённый оранжевый | редкие тёплые точечные акценты (charts) |
| `secondary` | тёплый светлый песочный | мягкие бейджи, плашки |
| `muted` / `muted-foreground` | тёплый бежевый / приглушённый коричневый | вторичный текст, фон секций, hover |
| `card` | чистый белый | панели `RestaurantPanel`, формы, таблицы |
| `border` | тёплый бежевый | тонкие границы (1px) везде |
Узнаваемые приёмы (держи их — это «лицо» шаблона):
- **Мягкие углы:** `--radius` = 0.5rem; всё на `rounded-md` (карточки, кнопки, фото-панели). Не используй острые `rounded-none` или сильно скруглённые pill-формы.
- **`RestaurantPanel`** (`src/shared/ui/restaurant-panel.tsx`) — фирменная панель: `rounded-md border border-border bg-card` + едва заметная тёплая тень `shadow-[0_1px_0_rgba(67,44,17,0.05)]`. Используй её для секционных карточек вместо голого `div`.
- **`Container`** (`src/shared/ui/container.tsx`) — `max-w-[1320px]` + `px-5 sm:px-8`; единый горизонтальный ритм страниц.
- **Hero-фон:** утилита `.restaurant-tablecloth` (тёплая «скатерть» — зелёно-охровая сетка-льняная фактура 42px с mask-fade) для главного hero.
- **Serif-дисплей:** заголовки через `font-display` (`text-4xl``text-[6.2rem]`, плотный `leading-[0.94]`); eyebrow-метки `text-xs font-semibold uppercase tracking-[0.18em] text-primary`.
- **Тонкие разделители:** `Separator` и `border-b border-border` в списках блюд/пакетов; структура на линиях, не на тенях.
Do / Don't:
- **Do:** держи кремово-зелёную палитру и терракотовый акцент; serif-заголовки + Manrope-текст; оборачивай секции в `Container`/`RestaurantPanel`; продавай ресторанную специфику (посадка, pairing, аллергены, тайминг вечера).
- **Don't:** не вводи холодный синий/чёрный primary, неоновые градиенты, тяжёлые drop-shadow, острые углы или generic-SaaS hero — это ломает тёплый fine-dining тон.
## File Map
| Route | Widget |
|---|---|
| `/` | `src/widgets/home-page.tsx` (`HomePage`) |
| `/menu` | `src/widgets/menu-page.tsx` (`MenuPage`) |
| `/reservations` | `src/widgets/reservations-page.tsx` (`ReservationsPage`) |
| `/private-events` | `src/widgets/private-events-page.tsx` (`PrivateEventsPage`) |
| `/about` | `src/widgets/about-page.tsx` (`AboutPage`) |
| `/contact` | `src/widgets/contact-page.tsx` (`ContactPage`) |
Переиспользуемые блоки:
- `src/widgets/site-shell.tsx``SiteHeader` + `SiteFooter` (обёртка из `src/app/layout.tsx`).
- `src/widgets/menu-section-board.tsx``MenuSectionBoard` (Home, Menu).
- `src/widgets/dish-showcase.tsx``DishShowcase` (Home, Menu; локальный `DishRow` внутри).
- `src/widgets/seating-guide.tsx``SeatingGuide` (Home, Reservations).
- `src/widgets/restaurant-evening-plan.tsx``RestaurantEveningPlan` (Home, Reservations).
- `src/widgets/private-events-grid.tsx``PrivateEventsGrid` (Home, Private events).
- `src/widgets/testimonial-band.tsx``TestimonialBand` (Home, About).
- `src/shared/ui/container.tsx``Container` (горизонтальный wrapper всех секций).
- `src/shared/ui/restaurant-panel.tsx``RestaurantPanel` (фирменная карточка-панель).
- `src/shared/ui/section-header.tsx``SectionHeader` (eyebrow + заголовок секции).
- `src/shared/ui/inner-hero.tsx``InnerHero` (хедер всех внутренних страниц).
- `src/shared/ui/info-columns.tsx``InfoColumns` (правила/dietary notes; используется в Menu и Reservations).
- `src/shared/ui/cta-panel.tsx``CtaPanel` (Menu, Private events).
- `src/shared/ui/split-story.tsx``SplitStory` (готовый props-блок image+points, сейчас не подключён ни одной страницей).
Одноразовые блоки колоцированы со своей страницей:
- `PageHero` — внутри `home-page.tsx`.
- `FeaturedGrid`, `PairingBoard` (+ `WineIcon`), `PricingTiles`, `DietaryNotes` — внутри `menu-page.tsx`.
- `ReservationForm`, `ReservationRules` — внутри `reservations-page.tsx`.
- `ChefStory`, `SupplierLedger`, `MetricStrip` — внутри `about-page.tsx`.
- `IconCards`, `HoursBoard`, `ContactForm` — внутри `contact-page.tsx`.
- `EventPlanner` — внутри `private-events-page.tsx`.

View File

@@ -1,19 +1,5 @@
"use client"; import { AboutPage } from "@/widgets/about-page";
import { ChefStory, InnerHero, MetricStrip, SupplierLedger, TestimonialBand } from "@/widgets/template-ui";
export default function Page() { export default function Page() {
return ( return <AboutPage />;
<>
<InnerHero
eyebrow="Kitchen"
title="Juniper Table держится на продукте, сезоне и спокойном сервисе"
text="About page объясняет кухню, поставщиков, открытый формат и почему меню меняется каждые несколько недель."
/>
<ChefStory />
<SupplierLedger />
<MetricStrip />
<TestimonialBand />
</>
);
} }

View File

@@ -1,19 +1,5 @@
"use client"; import { ContactPage } from "@/widgets/contact-page";
import { ContactForm, HoursBoard, IconCards, InnerHero } from "@/widgets/template-ui";
import { contactCards } from "@/entities/site-content";
export default function Page() { export default function Page() {
return ( return <ContactPage />;
<>
<InnerHero
eyebrow="Contact"
title="Адрес, часы кухни, хост и детали подтверждения"
text="Контакты ресторана должны отвечать на вопросы до звонка: как добраться, когда открыта кухня, как подтвердить бронь и где указать ограничения."
/>
<IconCards items={contactCards} />
<HoursBoard />
<ContactForm />
</>
);
} }

View File

@@ -3,7 +3,7 @@ import { Cormorant_Garamond, Manrope } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { ThemeProvider } from "@/shared/hooks/theme-provider"; import { ThemeProvider } from "@/shared/hooks/theme-provider";
import { ThemeMessageListener } from "@/shared/hooks/theme-message-listener"; import { ThemeMessageListener } from "@/shared/hooks/theme-message-listener";
import { SiteHeader, SiteFooter } from "@/widgets/template-ui"; import { SiteHeader, SiteFooter } from "@/widgets/site-shell";
const display = Cormorant_Garamond({ const display = Cormorant_Garamond({
variable: "--font-display", variable: "--font-display",

View File

@@ -1,35 +1,5 @@
"use client"; import { MenuPage } from "@/widgets/menu-page";
import {
CtaPanel,
DietaryNotes,
DishShowcase,
FeaturedGrid,
InnerHero,
MenuSectionBoard,
PairingBoard,
PricingTiles,
} from "@/widgets/template-ui";
export default function Page() { export default function Page() {
return ( return <MenuPage />;
<>
<InnerHero
eyebrow="Seasonal menu"
title="Меню показывает сценарий ужина, аллергены и pairing"
text="Страница меню продает не набор карточек, а путь гостя: секции, блюда, ограничения, напитки и дегустационные форматы."
/>
<MenuSectionBoard />
<DishShowcase />
<FeaturedGrid
eyebrow="Menu ledger"
title="A la carte с полезными деталями"
text="У каждого блюда есть секция, цена, краткое описание и pairing, чтобы гость принимал решение до бронирования."
/>
<PairingBoard />
<DietaryNotes />
<PricingTiles title="Дегустационные форматы" />
<CtaPanel title="Нужен стол у открытой кухни?" text="Укажите дату, время, гостей и ограничения. Хост подтвердит доступный формат посадки." href="/reservations" label="Перейти к брони" />
</>
);
} }

View File

@@ -1,25 +1,5 @@
"use client"; import { HomePage } from "@/widgets/home-page";
import {
DishShowcase,
MenuSectionBoard,
PageHero,
PrivateEventsGrid,
RestaurantEveningPlan,
SeatingGuide,
TestimonialBand,
} from "@/widgets/template-ui";
export default function Page() { export default function Page() {
return ( return <HomePage />;
<>
<PageHero />
<MenuSectionBoard />
<DishShowcase />
<SeatingGuide />
<RestaurantEveningPlan />
<PrivateEventsGrid />
<TestimonialBand />
</>
);
} }

View File

@@ -1,18 +1,5 @@
"use client"; import { PrivateEventsPage } from "@/widgets/private-events-page";
import { CtaPanel, EventPlanner, InnerHero, PrivateEventsGrid } from "@/widgets/template-ui";
export default function Page() { export default function Page() {
return ( return <PrivateEventsPage />;
<>
<InnerHero
eyebrow="Private dining"
title="Камерные ужины, wedding welcome и corporate dinner как отдельные продукты"
text="Страница продает высокомаржинальные сценарии: гости, формат, меню, депозит, тайминг и команда зала."
/>
<PrivateEventsGrid />
<EventPlanner />
<CtaPanel title="Собрать вечер под вашу дату?" text="Оставьте формат, гостей и желаемый тайминг. Команда предложит меню и условия." href="/reservations" label="Оставить запрос" />
</>
);
} }

View File

@@ -1,19 +1,5 @@
"use client"; import { ReservationsPage } from "@/widgets/reservations-page";
import { InnerHero, ReservationForm, ReservationRules, RestaurantEveningPlan, SeatingGuide } from "@/widgets/template-ui";
export default function Page() { export default function Page() {
return ( return <ReservationsPage />;
<>
<InnerHero
eyebrow="Reservations"
title="Бронь уточняет формат вечера, посадку и ограничения"
text="Production-ready booking UI для ресторана: гости, время, посадка, событие, аллергии и правила подтверждения."
/>
<SeatingGuide />
<ReservationForm />
<ReservationRules />
<RestaurantEveningPlan />
</>
);
} }

View File

@@ -0,0 +1,5 @@
import type { ReactNode } from "react";
export function Container({ children, className = "" }: { children: ReactNode; className?: string }) {
return <section className={"mx-auto w-full max-w-[1320px] px-5 sm:px-8 " + className}>{children}</section>;
}

View File

@@ -0,0 +1,19 @@
import Link from "next/link";
import { ArrowRightIcon } from "lucide-react";
import { Button } from "@/shared/ui/button";
import { Container } from "@/shared/ui/container";
export function CtaPanel({ title, text, href, label }: { title: string; text: string; href: string; label: string }) {
return (
<Container className="py-12">
<div className="rounded-md bg-primary p-8 text-primary-foreground">
<h2 className="font-display text-4xl font-semibold">{title}</h2>
<p className="mt-4 max-w-[720px] text-sm leading-7 text-primary-foreground/72">{text}</p>
<Button asChild variant="secondary" className="mt-6 rounded-md">
<Link href={href}>{label}<ArrowRightIcon className="size-4" /></Link>
</Button>
</div>
</Container>
);
}

View File

@@ -0,0 +1,23 @@
import { StarIcon } from "lucide-react";
import { Container } from "@/shared/ui/container";
import { RestaurantPanel } from "@/shared/ui/restaurant-panel";
type InfoItem = { title: string; text: string };
export function InfoColumns({ title, items }: { title: string; items: readonly InfoItem[] }) {
return (
<Container className="py-12">
<h2 className="mb-6 font-display text-4xl font-semibold">{title}</h2>
<div className="grid gap-4 md:grid-cols-3">
{items.map((item) => (
<RestaurantPanel key={item.title} className="p-6">
<StarIcon className="mb-8 size-5 text-primary" />
<h3 className="font-display text-2xl font-semibold">{item.title}</h3>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
</RestaurantPanel>
))}
</div>
</Container>
);
}

View File

@@ -0,0 +1,14 @@
import { Badge } from "@/shared/ui/badge";
import { Container } from "@/shared/ui/container";
export function InnerHero({ eyebrow, title, text }: { eyebrow: string; title: string; text: string }) {
return (
<section className="border-b border-border bg-card">
<Container className="py-14">
<Badge variant="outline" className="mb-6 rounded-md border-primary/30 text-primary">{eyebrow}</Badge>
<h1 className="max-w-[980px] font-display text-4xl font-semibold leading-[1.02] sm:text-6xl">{title}</h1>
<p className="mt-5 max-w-[760px] text-base leading-7 text-muted-foreground">{text}</p>
</Container>
</section>
);
}

View File

@@ -0,0 +1,5 @@
import type { ReactNode } from "react";
export function RestaurantPanel({ children, className = "" }: { children: ReactNode; className?: string }) {
return <div className={"rounded-md border border-border bg-card shadow-[0_1px_0_rgba(67,44,17,0.05)] " + className}>{children}</div>;
}

View File

@@ -0,0 +1,11 @@
export function SectionHeader({ eyebrow, title, text }: { eyebrow: string; title: string; text?: string }) {
return (
<div className="mb-8 grid gap-4 md:grid-cols-[0.8fr_1fr] md:items-end">
<div>
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-primary">{eyebrow}</div>
<h2 className="mt-2 font-display text-4xl font-semibold leading-tight sm:text-6xl">{title}</h2>
</div>
{text ? <p className="max-w-[620px] text-sm leading-7 text-muted-foreground sm:text-base">{text}</p> : null}
</div>
);
}

View File

@@ -0,0 +1,28 @@
import Image from "next/image";
import { CheckIcon } from "lucide-react";
import { Container } from "@/shared/ui/container";
import { RestaurantPanel } from "@/shared/ui/restaurant-panel";
export function SplitStory({ image, eyebrow, title, text, points }: { image: string; eyebrow: string; title: string; text: string; points: readonly string[] }) {
return (
<Container className="grid gap-10 py-14 lg:grid-cols-2">
<RestaurantPanel className="relative min-h-[500px] overflow-hidden bg-muted">
<Image src={image} alt={title} fill className="object-cover" sizes="(min-width: 1024px) 50vw, 100vw" />
</RestaurantPanel>
<div className="flex flex-col justify-center">
<div className="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-primary">{eyebrow}</div>
<h2 className="font-display text-4xl font-semibold leading-tight sm:text-5xl">{title}</h2>
<p className="mt-5 text-base leading-7 text-muted-foreground">{text}</p>
<div className="mt-7 grid gap-3">
{points.map((point) => (
<div key={point} className="flex items-center gap-3 text-sm font-semibold">
<CheckIcon className="size-4 text-primary" />
{point}
</div>
))}
</div>
</div>
</Container>
);
}

View File

@@ -0,0 +1,85 @@
import Image from "next/image";
import { CheckIcon } from "lucide-react";
import { highlights, suppliers } from "@/entities/site-content";
import { Container } from "@/shared/ui/container";
import { InnerHero } from "@/shared/ui/inner-hero";
import { RestaurantPanel } from "@/shared/ui/restaurant-panel";
import { SectionHeader } from "@/shared/ui/section-header";
import { TestimonialBand } from "@/widgets/testimonial-band";
function ChefStory() {
return (
<Container className="grid gap-10 py-14 lg:grid-cols-2">
<RestaurantPanel className="relative min-h-[520px] overflow-hidden bg-muted">
<Image src="https://images.unsplash.com/photo-1577219491135-ce391730fb2c?auto=format&fit=crop&w=1200&q=82" alt="Kitchen team" fill className="object-cover" sizes="(min-width: 1024px) 50vw, 100vw" />
</RestaurantPanel>
<div className="flex flex-col justify-center">
<div className="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-primary">Kitchen story</div>
<h2 className="font-display text-4xl font-semibold leading-tight sm:text-5xl">Точная кухня и тихая уверенность сервиса</h2>
<p className="mt-5 text-base leading-7 text-muted-foreground">
Juniper Table строит меню вокруг короткого сезонного окна. Шеф-команда не обещает шоу: она держит продукт, соус, свет и темп вечера.
</p>
<div className="mt-7 grid gap-3">
{["поставки под меню недели", "открытая кухня без театральности", "зал держит ритм, а не давит на гостя"].map((point) => (
<div key={point} className="flex items-center gap-3 text-sm font-semibold">
<CheckIcon className="size-4 text-primary" />
{point}
</div>
))}
</div>
</div>
</Container>
);
}
function SupplierLedger() {
return (
<Container className="py-12">
<SectionHeader eyebrow="Suppliers" title="Поставщики объясняют сезонность меню" text="Ресторанный сайт становится убедительнее, когда показывает, откуда берутся продукты и почему меню меняется." />
<div className="overflow-hidden rounded-md border border-border bg-card">
{suppliers.map((supplier) => (
<div key={supplier.name} className="grid gap-3 border-b border-border p-5 last:border-b-0 md:grid-cols-[1fr_1fr_180px]">
<div className="font-display text-2xl font-semibold">{supplier.name}</div>
<div className="text-sm text-muted-foreground">{supplier.product}</div>
<div className="text-sm font-semibold text-primary md:text-right">{supplier.cadence}</div>
</div>
))}
</div>
</Container>
);
}
function MetricStrip() {
return (
<Container className="grid gap-3 py-8 md:grid-cols-3">
{highlights.map((item) => {
const Icon = item.icon;
return (
<RestaurantPanel key={item.title} className="p-5">
<Icon className="mb-6 size-5 text-primary" />
<div className="font-display text-4xl font-semibold">{item.value}</div>
<h3 className="mt-2 font-semibold">{item.title}</h3>
<p className="mt-2 text-sm leading-6 text-muted-foreground">{item.text}</p>
</RestaurantPanel>
);
})}
</Container>
);
}
export function AboutPage() {
return (
<>
<InnerHero
eyebrow="Kitchen"
title="Juniper Table держится на продукте, сезоне и спокойном сервисе"
text="About page объясняет кухню, поставщиков, открытый формат и почему меню меняется каждые несколько недель."
/>
<ChefStory />
<SupplierLedger />
<MetricStrip />
<TestimonialBand />
</>
);
}

View File

@@ -0,0 +1,82 @@
import type { ComponentType } from "react";
import { ClockIcon } from "lucide-react";
import { contactCards, serviceWindows } from "@/entities/site-content";
import { Button } from "@/shared/ui/button";
import { Container } from "@/shared/ui/container";
import { InnerHero } from "@/shared/ui/inner-hero";
import { Input } from "@/shared/ui/input";
import { RestaurantPanel } from "@/shared/ui/restaurant-panel";
import { Textarea } from "@/shared/ui/textarea";
type IconComponent = ComponentType<{ className?: string }>;
type IconItem = { title: string; text: string; icon: IconComponent };
function IconCards({ items }: { items: readonly IconItem[] }) {
return (
<Container className="grid gap-4 py-12 md:grid-cols-3">
{items.map((item) => {
const Icon = item.icon;
return (
<RestaurantPanel key={item.title} className="p-6">
<Icon className="mb-8 size-6 text-primary" />
<h3 className="font-display text-2xl font-semibold">{item.title}</h3>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
</RestaurantPanel>
);
})}
</Container>
);
}
function HoursBoard() {
return (
<Container className="py-12">
<div className="grid gap-4 md:grid-cols-3">
{serviceWindows.map((item) => (
<RestaurantPanel key={item.label} className="p-6">
<ClockIcon className="mb-8 size-6 text-primary" />
<h3 className="font-display text-3xl font-semibold">{item.label}</h3>
<div className="mt-3 font-display text-3xl font-semibold text-primary">{item.time}</div>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.note}</p>
</RestaurantPanel>
))}
</div>
</Container>
);
}
function ContactForm() {
return (
<Container className="grid gap-6 py-14 lg:grid-cols-2">
<RestaurantPanel className="p-6">
<ClockIcon className="mb-8 size-6 text-primary" />
<h2 className="font-display text-4xl font-semibold">Связаться с хостом</h2>
<p className="mt-3 text-sm leading-7 text-muted-foreground">
Контактная страница закрывает практику: как добраться, когда работает кухня, как подтвердить бронь и где указать ограничения.
</p>
</RestaurantPanel>
<form className="grid gap-3 rounded-md border border-border bg-card p-5">
<Input placeholder="Ваше имя" />
<Input placeholder="Email или телефон" />
<Textarea placeholder="Дата, гости, событие, аллергии или вопрос хосту" />
<Button type="button" className="h-11 rounded-md">Отправить</Button>
</form>
</Container>
);
}
export function ContactPage() {
return (
<>
<InnerHero
eyebrow="Contact"
title="Адрес, часы кухни, хост и детали подтверждения"
text="Контакты ресторана должны отвечать на вопросы до звонка: как добраться, когда открыта кухня, как подтвердить бронь и где указать ограничения."
/>
<IconCards items={contactCards} />
<HoursBoard />
<ContactForm />
</>
);
}

View File

@@ -0,0 +1,58 @@
import Image from "next/image";
import { dishes } from "@/entities/site-content";
import { Badge } from "@/shared/ui/badge";
import { Container } from "@/shared/ui/container";
import { RestaurantPanel } from "@/shared/ui/restaurant-panel";
import { SectionHeader } from "@/shared/ui/section-header";
type DishItem = (typeof dishes)[number];
function DishRow({ dish }: { dish: DishItem }) {
return (
<article className="grid grid-cols-[132px_1fr] overflow-hidden rounded-md border border-border bg-card sm:grid-cols-[170px_1fr]">
<div className="relative min-h-[168px] bg-muted">
<Image src={dish.image} alt={dish.name} fill className="object-cover" sizes="180px" />
</div>
<div className="p-4">
<div className="mb-3 flex flex-wrap items-center gap-2">
<Badge variant="outline" className="rounded-md">{dish.section}</Badge>
<span className="text-sm font-semibold">{dish.price}</span>
</div>
<h3 className="font-display text-2xl font-semibold">{dish.name}</h3>
<p className="mt-2 text-sm leading-6 text-muted-foreground">{dish.text}</p>
<div className="mt-4 grid gap-2 text-xs text-muted-foreground sm:grid-cols-2">
<div>allergens: {dish.allergens}</div>
<div>pairing: {dish.pairing}</div>
</div>
</div>
</article>
);
}
export function DishShowcase() {
const heroDish = dishes[1];
return (
<Container className="py-14">
<SectionHeader eyebrow="A la carte" title="Блюда продают вкус, ограничения и pairing сразу" text="Для ресторанного шаблона важно показывать аллергенные подсказки, секцию меню и сочетание напитков, а не только фото и цену." />
<div className="grid gap-4 lg:grid-cols-[1.05fr_0.95fr]">
<RestaurantPanel className="relative min-h-[560px] overflow-hidden bg-muted">
<Image src={heroDish.image} alt={heroDish.name} fill className="object-cover" sizes="(min-width: 1024px) 54vw, 100vw" />
<div className="absolute bottom-5 left-5 right-5 rounded-md bg-background/92 p-5 backdrop-blur">
<Badge variant="secondary" className="mb-3 rounded-md">{heroDish.tag}</Badge>
<div className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
<h3 className="font-display text-3xl font-semibold">{heroDish.name}</h3>
<div className="font-display text-3xl font-semibold">{heroDish.price}</div>
</div>
<p className="mt-2 text-sm text-muted-foreground">{heroDish.text}</p>
</div>
</RestaurantPanel>
<div className="grid gap-3">
{dishes.filter((dish) => dish.name !== heroDish.name).map((dish) => (
<DishRow key={dish.name} dish={dish} />
))}
</div>
</div>
</Container>
);
}

91
src/widgets/home-page.tsx Normal file
View File

@@ -0,0 +1,91 @@
import Image from "next/image";
import Link from "next/link";
import { ArrowRightIcon } from "lucide-react";
import { highlights, serviceWindows, site } from "@/entities/site-content";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { Container } from "@/shared/ui/container";
import { RestaurantPanel } from "@/shared/ui/restaurant-panel";
import { DishShowcase } from "@/widgets/dish-showcase";
import { MenuSectionBoard } from "@/widgets/menu-section-board";
import { PrivateEventsGrid } from "@/widgets/private-events-grid";
import { RestaurantEveningPlan } from "@/widgets/restaurant-evening-plan";
import { SeatingGuide } from "@/widgets/seating-guide";
import { TestimonialBand } from "@/widgets/testimonial-band";
function PageHero() {
return (
<section className="relative overflow-hidden border-b border-border">
<div className="restaurant-tablecloth absolute inset-0" />
<Container className="relative grid min-h-[760px] gap-10 py-14 lg:grid-cols-[0.72fr_1.28fr]">
<div className="flex flex-col justify-center">
<Badge variant="outline" className="mb-6 w-fit rounded-md border-primary/30 bg-background/80 text-primary">service opens at 18:00</Badge>
<h1 className="font-display text-5xl font-semibold leading-[0.94] tracking-normal sm:text-7xl lg:text-[6.2rem]">
Вечер начинается до первого блюда
</h1>
<p className="mt-6 max-w-[580px] text-lg leading-8 text-muted-foreground">{site.tagline}</p>
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<Button asChild className="h-12 rounded-md px-6">
<Link href="/reservations">{site.cta}<ArrowRightIcon className="size-4" /></Link>
</Button>
<Button asChild variant="outline" className="h-12 rounded-md bg-background/65 px-6">
<Link href="/menu">{site.secondaryCta}</Link>
</Button>
</div>
<div className="mt-9 grid gap-3 sm:grid-cols-3">
{serviceWindows.map((window) => (
<div key={window.label} className="rounded-md border border-border bg-card/88 p-4">
<div className="font-display text-2xl font-semibold">{window.time}</div>
<div className="mt-1 text-sm font-semibold">{window.label}</div>
<div className="mt-1 text-xs leading-5 text-muted-foreground">{window.note}</div>
</div>
))}
</div>
</div>
<div className="grid gap-4 xl:grid-cols-[1fr_300px]">
<RestaurantPanel className="relative min-h-[620px] overflow-hidden bg-muted">
<Image src={site.heroImage} alt="Вечерняя посадка в Juniper Table" fill priority className="object-cover" sizes="(min-width: 1280px) 680px, 100vw" />
<div className="absolute bottom-5 left-5 right-5 rounded-md bg-background/92 p-5 shadow-xl backdrop-blur">
<div className="flex items-start justify-between gap-4">
<div>
<Badge variant="secondary" className="mb-3 rounded-md">tonight</Badge>
<h2 className="font-display text-3xl font-semibold">Chef counter: 4 места на 20:30</h2>
<p className="mt-2 text-sm leading-6 text-muted-foreground">Подходит для дегустационного сета и pairing.</p>
</div>
<div className="font-display text-2xl font-semibold">18</div>
</div>
</div>
</RestaurantPanel>
<div className="grid content-center gap-4">
{highlights.map((item) => {
const Icon = item.icon;
return (
<RestaurantPanel key={item.title} className="p-5">
<Icon className="mb-7 size-5 text-primary" />
<div className="font-display text-3xl font-semibold">{item.value}</div>
<h3 className="mt-2 font-semibold">{item.title}</h3>
<p className="mt-2 text-sm leading-6 text-muted-foreground">{item.text}</p>
</RestaurantPanel>
);
})}
</div>
</div>
</Container>
</section>
);
}
export function HomePage() {
return (
<>
<PageHero />
<MenuSectionBoard />
<DishShowcase />
<SeatingGuide />
<RestaurantEveningPlan />
<PrivateEventsGrid />
<TestimonialBand />
</>
);
}

110
src/widgets/menu-page.tsx Normal file
View File

@@ -0,0 +1,110 @@
import { CheckIcon, StarIcon } from "lucide-react";
import { dietaryNotes, dishes, pairingFlights, tastingSets } from "@/entities/site-content";
import { Container } from "@/shared/ui/container";
import { CtaPanel } from "@/shared/ui/cta-panel";
import { InfoColumns } from "@/shared/ui/info-columns";
import { InnerHero } from "@/shared/ui/inner-hero";
import { RestaurantPanel } from "@/shared/ui/restaurant-panel";
import { SectionHeader } from "@/shared/ui/section-header";
import { Separator } from "@/shared/ui/separator";
import { DishShowcase } from "@/widgets/dish-showcase";
import { MenuSectionBoard } from "@/widgets/menu-section-board";
type DishItem = (typeof dishes)[number];
type TileItem = { title: string; price: string; items: readonly string[] };
function FeaturedGrid({ eyebrow, title, text, items = dishes }: { eyebrow: string; title: string; text: string; items?: readonly DishItem[] }) {
return (
<Container className="py-14">
<SectionHeader eyebrow={eyebrow} title={title} text={text} />
<div className="overflow-hidden rounded-md border border-border bg-card">
{items.map((item) => (
<div key={item.name} className="grid gap-4 border-b border-border p-5 last:border-b-0 md:grid-cols-[1fr_140px_180px] md:items-center">
<div>
<div className="mb-2 text-xs uppercase tracking-[0.14em] text-primary">{item.tag}</div>
<h3 className="font-display text-2xl font-semibold">{item.name}</h3>
<p className="mt-2 text-sm leading-6 text-muted-foreground">{item.text}</p>
</div>
<div className="font-display text-2xl font-semibold md:text-right">{item.price}</div>
<div className="text-sm leading-6 text-muted-foreground">pairing: {item.pairing}</div>
</div>
))}
</div>
</Container>
);
}
function WineIcon({ className }: { className?: string }) {
return <StarIcon className={className} />;
}
function PairingBoard() {
return (
<Container className="py-14">
<SectionHeader eyebrow="Pairing" title="Напитки продают темп вечера" text="Pairing вынесен отдельным блоком: вино, non-alcohol и спокойные альтернативы без давления на гостя." />
<div className="grid gap-4 md:grid-cols-3">
{pairingFlights.map((flight) => (
<RestaurantPanel key={flight.title} className="p-6">
<WineIcon className="mb-8 size-6 text-primary" />
<h3 className="font-display text-3xl font-semibold">{flight.title}</h3>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{flight.text}</p>
<div className="mt-5 font-display text-2xl font-semibold">{flight.price}</div>
</RestaurantPanel>
))}
</div>
</Container>
);
}
function PricingTiles({ title, items = tastingSets }: { title: string; items?: readonly TileItem[] }) {
return (
<Container className="py-14">
<h2 className="mb-7 font-display text-4xl font-semibold sm:text-5xl">{title}</h2>
<div className="grid gap-4 md:grid-cols-3">
{items.map((item) => (
<RestaurantPanel key={item.title} className="p-6">
<h3 className="font-display text-2xl font-semibold">{item.title}</h3>
<div className="mt-3 font-display text-3xl font-semibold text-primary">{item.price}</div>
<Separator className="my-5" />
<div className="grid gap-2">
{item.items.map((line) => (
<div key={line} className="flex items-center gap-2 text-sm text-muted-foreground">
<CheckIcon className="size-4 text-primary" />
{line}
</div>
))}
</div>
</RestaurantPanel>
))}
</div>
</Container>
);
}
function DietaryNotes() {
return <InfoColumns title="Allergies and dietary notes" items={dietaryNotes} />;
}
export function MenuPage() {
return (
<>
<InnerHero
eyebrow="Seasonal menu"
title="Меню показывает сценарий ужина, аллергены и pairing"
text="Страница меню продает не набор карточек, а путь гостя: секции, блюда, ограничения, напитки и дегустационные форматы."
/>
<MenuSectionBoard />
<DishShowcase />
<FeaturedGrid
eyebrow="Menu ledger"
title="A la carte с полезными деталями"
text="У каждого блюда есть секция, цена, краткое описание и pairing, чтобы гость принимал решение до бронирования."
/>
<PairingBoard />
<DietaryNotes />
<PricingTiles title="Дегустационные форматы" />
<CtaPanel title="Нужен стол у открытой кухни?" text="Укажите дату, время, гостей и ограничения. Хост подтвердит доступный формат посадки." href="/reservations" label="Перейти к брони" />
</>
);
}

View File

@@ -0,0 +1,32 @@
import { CheckIcon } from "lucide-react";
import { menuSections } from "@/entities/site-content";
import { Container } from "@/shared/ui/container";
import { RestaurantPanel } from "@/shared/ui/restaurant-panel";
import { SectionHeader } from "@/shared/ui/section-header";
import { Separator } from "@/shared/ui/separator";
export function MenuSectionBoard() {
return (
<Container className="py-14">
<SectionHeader eyebrow="Menu architecture" title="Меню собрано по сценарию вечера" text="Гость видит не хаотичный список блюд, а путь: легкий старт, огонь и основные блюда, затем десерт и pairing." />
<div className="grid gap-4 md:grid-cols-3">
{menuSections.map((section) => (
<RestaurantPanel key={section.title} className="p-6">
<h3 className="font-display text-3xl font-semibold">{section.title}</h3>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{section.note}</p>
<Separator className="my-5" />
<div className="grid gap-2">
{section.items.map((item) => (
<div key={item} className="flex items-center gap-2 text-sm text-muted-foreground">
<CheckIcon className="size-4 text-primary" />
{item}
</div>
))}
</div>
</RestaurantPanel>
))}
</div>
</Container>
);
}

View File

@@ -0,0 +1,33 @@
import { CheckIcon } from "lucide-react";
import { privatePackages } from "@/entities/site-content";
import { Container } from "@/shared/ui/container";
import { RestaurantPanel } from "@/shared/ui/restaurant-panel";
import { SectionHeader } from "@/shared/ui/section-header";
import { Separator } from "@/shared/ui/separator";
export function PrivateEventsGrid() {
return (
<Container className="py-14">
<SectionHeader eyebrow="Private dining" title="События продаются пакетами, а не одной формой обратной связи" text="Для ресторана private events - отдельная выручка: гости, формат, депозит, меню и тайминг должны быть видны заранее." />
<div className="grid gap-4 md:grid-cols-3">
{privatePackages.map((item) => (
<RestaurantPanel key={item.title} className="p-6">
<h3 className="font-display text-3xl font-semibold">{item.title}</h3>
<div className="mt-3 text-sm font-semibold text-primary">{item.guests}</div>
<div className="mt-5 font-display text-3xl font-semibold">{item.price}</div>
<Separator className="my-5" />
<div className="grid gap-2">
{item.details.map((detail) => (
<div key={detail} className="flex items-center gap-2 text-sm text-muted-foreground">
<CheckIcon className="size-4 text-primary" />
{detail}
</div>
))}
</div>
</RestaurantPanel>
))}
</div>
</Container>
);
}

View File

@@ -0,0 +1,45 @@
import Image from "next/image";
import { eventTypes } from "@/entities/site-content";
import { Container } from "@/shared/ui/container";
import { CtaPanel } from "@/shared/ui/cta-panel";
import { InnerHero } from "@/shared/ui/inner-hero";
import { RestaurantPanel } from "@/shared/ui/restaurant-panel";
import { PrivateEventsGrid } from "@/widgets/private-events-grid";
function EventPlanner() {
return (
<Container className="grid gap-4 py-12 lg:grid-cols-[1fr_1.05fr]">
<RestaurantPanel className="relative min-h-[460px] overflow-hidden bg-muted">
<Image src="https://images.unsplash.com/photo-1464366400600-7168b8af9bc3?auto=format&fit=crop&w=1200&q=82" alt="Private dining room" fill className="object-cover" sizes="(min-width: 1024px) 50vw, 100vw" />
</RestaurantPanel>
<div className="grid gap-4">
{eventTypes.map((item) => {
const Icon = item.icon;
return (
<RestaurantPanel key={item.title} className="p-6">
<Icon className="mb-7 size-6 text-primary" />
<h3 className="font-display text-3xl font-semibold">{item.title}</h3>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
</RestaurantPanel>
);
})}
</div>
</Container>
);
}
export function PrivateEventsPage() {
return (
<>
<InnerHero
eyebrow="Private dining"
title="Камерные ужины, wedding welcome и corporate dinner как отдельные продукты"
text="Страница продает высокомаржинальные сценарии: гости, формат, меню, депозит, тайминг и команда зала."
/>
<PrivateEventsGrid />
<EventPlanner />
<CtaPanel title="Собрать вечер под вашу дату?" text="Оставьте формат, гостей и желаемый тайминг. Команда предложит меню и условия." href="/reservations" label="Оставить запрос" />
</>
);
}

View File

@@ -0,0 +1,68 @@
import { CalendarDaysIcon } from "lucide-react";
import { bookingRules } from "@/entities/site-content";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { Container } from "@/shared/ui/container";
import { InfoColumns } from "@/shared/ui/info-columns";
import { InnerHero } from "@/shared/ui/inner-hero";
import { Input } from "@/shared/ui/input";
import { Textarea } from "@/shared/ui/textarea";
import { RestaurantEveningPlan } from "@/widgets/restaurant-evening-plan";
import { SeatingGuide } from "@/widgets/seating-guide";
function ReservationForm() {
return (
<Container className="grid gap-8 py-14 lg:grid-cols-[0.82fr_1.18fr]">
<div>
<Badge variant="outline" className="mb-5 rounded-md">
<CalendarDaysIcon className="size-4" />
booking flow
</Badge>
<h2 className="font-display text-4xl font-semibold sm:text-5xl">Бронь уточняет сценарий вечера</h2>
<p className="mt-4 text-sm leading-7 text-muted-foreground">
Форма статическая, но показывает production-ready сценарий: дата, время, гости, посадка, аллергии, событие и пожелания к темпу.
</p>
</div>
<form className="grid gap-4 rounded-md border border-border bg-card p-5">
<div className="grid gap-3 sm:grid-cols-2">
<Input placeholder="Имя" />
<Input placeholder="Телефон" />
<Input placeholder="Дата" />
<Input placeholder="Время" />
<Input placeholder="Количество гостей" />
<Input placeholder="Формат посадки" />
</div>
<div className="flex flex-wrap gap-2">
{["chef counter", "dining room", "private room", "tasting", "birthday"].map((tag) => (
<button key={tag} type="button" className="rounded-md border border-border bg-background px-3 py-2 text-sm font-medium text-muted-foreground">
{tag}
</button>
))}
</div>
<Textarea placeholder="Аллергии, dietary requests, детский стул, событие или пожелания к темпу вечера" />
<Button type="button" className="h-11 rounded-md">Отправить заявку хосту</Button>
</form>
</Container>
);
}
function ReservationRules() {
return <InfoColumns title="Правила бронирования" items={bookingRules} />;
}
export function ReservationsPage() {
return (
<>
<InnerHero
eyebrow="Reservations"
title="Бронь уточняет формат вечера, посадку и ограничения"
text="Production-ready booking UI для ресторана: гости, время, посадка, событие, аллергии и правила подтверждения."
/>
<SeatingGuide />
<ReservationForm />
<ReservationRules />
<RestaurantEveningPlan />
</>
);
}

View File

@@ -0,0 +1,21 @@
import { eveningTimeline } from "@/entities/site-content";
import { Container } from "@/shared/ui/container";
export function RestaurantEveningPlan() {
return (
<Container className="py-14">
<div className="rounded-md bg-primary p-6 text-primary-foreground sm:p-8">
<h2 className="font-display text-4xl font-semibold">Маршрут вечера</h2>
<div className="mt-6 grid gap-3 md:grid-cols-3">
{eveningTimeline.map((step) => (
<div key={step.time} className="rounded-md border border-primary-foreground/18 p-5">
<div className="font-display text-3xl font-semibold">{step.time}</div>
<h3 className="mt-5 font-semibold">{step.title}</h3>
<p className="mt-2 text-sm leading-6 text-primary-foreground/72">{step.text}</p>
</div>
))}
</div>
</div>
</Container>
);
}

View File

@@ -0,0 +1,22 @@
import { seatingOptions } from "@/entities/site-content";
import { Container } from "@/shared/ui/container";
import { RestaurantPanel } from "@/shared/ui/restaurant-panel";
import { SectionHeader } from "@/shared/ui/section-header";
export function SeatingGuide() {
return (
<Container className="py-14">
<SectionHeader eyebrow="Seating" title="Посадка - это отдельный продукт ресторана" text="Гость выбирает не просто время, а формат: зал, chef counter или приватная комната с депозитом и ожиданиями." />
<div className="grid gap-4 md:grid-cols-3">
{seatingOptions.map((option) => (
<RestaurantPanel key={option.title} className="p-6">
<h3 className="font-display text-3xl font-semibold">{option.title}</h3>
<div className="mt-3 font-display text-2xl font-semibold text-primary">{option.seats}</div>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{option.text}</p>
<div className="mt-5 rounded-md bg-muted px-3 py-2 text-sm text-muted-foreground">{option.deposit}</div>
</RestaurantPanel>
))}
</div>
</Container>
);
}

View File

@@ -0,0 +1,59 @@
import Link from "next/link";
import { MenuIcon } from "lucide-react";
import { site } from "@/entities/site-content";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
export function SiteHeader() {
return (
<header className="sticky top-0 z-50 border-b border-border bg-background/92 backdrop-blur-xl">
<div className="mx-auto flex h-[72px] w-full max-w-[1320px] items-center justify-between px-5 sm:px-8">
<Link href="/" className="flex items-center gap-3">
<span className="flex size-10 items-center justify-center rounded-md bg-primary text-sm font-bold text-primary-foreground">JT</span>
<span>
<span className="block font-display text-2xl font-semibold leading-none">{site.name}</span>
<span className="hidden text-[11px] uppercase tracking-[0.18em] text-muted-foreground sm:block">seasonal dining</span>
</span>
</Link>
<nav className="hidden items-center gap-1 lg:flex">
{site.nav.map((item) => (
<Link key={item.href} href={item.href} className="rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition hover:bg-muted hover:text-foreground">
{item.label}
</Link>
))}
</nav>
<div className="flex items-center gap-2">
<Badge variant="outline" className="hidden rounded-md border-primary/30 bg-card text-primary md:inline-flex">кухня до 23:00</Badge>
<Button asChild className="hidden h-11 rounded-md px-6 sm:inline-flex">
<Link href="/reservations">{site.cta}</Link>
</Button>
<Button variant="outline" size="icon" className="rounded-md lg:hidden" aria-label="Открыть меню">
<MenuIcon className="size-4" />
</Button>
</div>
</div>
</header>
);
}
export function SiteFooter() {
return (
<footer className="border-t border-border bg-primary text-primary-foreground">
<div className="mx-auto grid w-full max-w-[1320px] gap-8 px-5 py-12 sm:px-8 md:grid-cols-[1.4fr_1fr_1fr]">
<div>
<div className="font-display text-4xl font-semibold">{site.name}</div>
<p className="mt-4 max-w-[520px] text-sm leading-6 text-primary-foreground/72">{site.tagline}</p>
</div>
<div className="grid gap-2 text-sm text-primary-foreground/75">
<div className="mb-2 font-semibold text-primary-foreground">Страницы</div>
{site.nav.map((item) => <Link key={item.href} href={item.href}>{item.label}</Link>)}
</div>
<div className="text-sm leading-6 text-primary-foreground/75">
<div className="mb-2 font-semibold text-primary-foreground">Сегодня</div>
Chef counter бронируется заранее. Аллергии и dietary requests лучше указать до визита.
</div>
</div>
</footer>
);
}

View File

@@ -1,643 +0,0 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import type { ComponentType, ReactNode } from "react";
import {
ArrowRightIcon,
CalendarDaysIcon,
CheckIcon,
ClockIcon,
MenuIcon,
StarIcon,
} from "lucide-react";
import {
bookingRules,
dietaryNotes,
dishes,
eveningTimeline,
eventTypes,
highlights,
menuSections,
pairingFlights,
privatePackages,
seatingOptions,
serviceWindows,
site,
suppliers,
tastingSets,
testimonials,
} from "@/entities/site-content";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { Input } from "@/shared/ui/input";
import { Separator } from "@/shared/ui/separator";
import { Textarea } from "@/shared/ui/textarea";
type IconComponent = ComponentType<{ className?: string }>;
type IconItem = { title: string; text: string; icon: IconComponent };
type DishItem = (typeof dishes)[number];
type TileItem = { title: string; price: string; items: readonly string[] };
type InfoItem = { title: string; text: string };
type TestimonialItem = { name: string; text: string; rating: string };
function Container({ children, className = "" }: { children: ReactNode; className?: string }) {
return <section className={"mx-auto w-full max-w-[1320px] px-5 sm:px-8 " + className}>{children}</section>;
}
function RestaurantPanel({ children, className = "" }: { children: ReactNode; className?: string }) {
return <div className={"rounded-md border border-border bg-card shadow-[0_1px_0_rgba(67,44,17,0.05)] " + className}>{children}</div>;
}
function SectionHeader({ eyebrow, title, text }: { eyebrow: string; title: string; text?: string }) {
return (
<div className="mb-8 grid gap-4 md:grid-cols-[0.8fr_1fr] md:items-end">
<div>
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-primary">{eyebrow}</div>
<h2 className="mt-2 font-display text-4xl font-semibold leading-tight sm:text-6xl">{title}</h2>
</div>
{text ? <p className="max-w-[620px] text-sm leading-7 text-muted-foreground sm:text-base">{text}</p> : null}
</div>
);
}
export function SiteHeader() {
return (
<header className="sticky top-0 z-50 border-b border-border bg-background/92 backdrop-blur-xl">
<div className="mx-auto flex h-[72px] w-full max-w-[1320px] items-center justify-between px-5 sm:px-8">
<Link href="/" className="flex items-center gap-3">
<span className="flex size-10 items-center justify-center rounded-md bg-primary text-sm font-bold text-primary-foreground">JT</span>
<span>
<span className="block font-display text-2xl font-semibold leading-none">{site.name}</span>
<span className="hidden text-[11px] uppercase tracking-[0.18em] text-muted-foreground sm:block">seasonal dining</span>
</span>
</Link>
<nav className="hidden items-center gap-1 lg:flex">
{site.nav.map((item) => (
<Link key={item.href} href={item.href} className="rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition hover:bg-muted hover:text-foreground">
{item.label}
</Link>
))}
</nav>
<div className="flex items-center gap-2">
<Badge variant="outline" className="hidden rounded-md border-primary/30 bg-card text-primary md:inline-flex">кухня до 23:00</Badge>
<Button asChild className="hidden h-11 rounded-md px-6 sm:inline-flex">
<Link href="/reservations">{site.cta}</Link>
</Button>
<Button variant="outline" size="icon" className="rounded-md lg:hidden" aria-label="Открыть меню">
<MenuIcon className="size-4" />
</Button>
</div>
</div>
</header>
);
}
export function SiteFooter() {
return (
<footer className="border-t border-border bg-primary text-primary-foreground">
<div className="mx-auto grid w-full max-w-[1320px] gap-8 px-5 py-12 sm:px-8 md:grid-cols-[1.4fr_1fr_1fr]">
<div>
<div className="font-display text-4xl font-semibold">{site.name}</div>
<p className="mt-4 max-w-[520px] text-sm leading-6 text-primary-foreground/72">{site.tagline}</p>
</div>
<div className="grid gap-2 text-sm text-primary-foreground/75">
<div className="mb-2 font-semibold text-primary-foreground">Страницы</div>
{site.nav.map((item) => <Link key={item.href} href={item.href}>{item.label}</Link>)}
</div>
<div className="text-sm leading-6 text-primary-foreground/75">
<div className="mb-2 font-semibold text-primary-foreground">Сегодня</div>
Chef counter бронируется заранее. Аллергии и dietary requests лучше указать до визита.
</div>
</div>
</footer>
);
}
export function PageHero() {
return (
<section className="relative overflow-hidden border-b border-border">
<div className="restaurant-tablecloth absolute inset-0" />
<Container className="relative grid min-h-[760px] gap-10 py-14 lg:grid-cols-[0.72fr_1.28fr]">
<div className="flex flex-col justify-center">
<Badge variant="outline" className="mb-6 w-fit rounded-md border-primary/30 bg-background/80 text-primary">service opens at 18:00</Badge>
<h1 className="font-display text-5xl font-semibold leading-[0.94] tracking-normal sm:text-7xl lg:text-[6.2rem]">
Вечер начинается до первого блюда
</h1>
<p className="mt-6 max-w-[580px] text-lg leading-8 text-muted-foreground">{site.tagline}</p>
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<Button asChild className="h-12 rounded-md px-6">
<Link href="/reservations">{site.cta}<ArrowRightIcon className="size-4" /></Link>
</Button>
<Button asChild variant="outline" className="h-12 rounded-md bg-background/65 px-6">
<Link href="/menu">{site.secondaryCta}</Link>
</Button>
</div>
<div className="mt-9 grid gap-3 sm:grid-cols-3">
{serviceWindows.map((window) => (
<div key={window.label} className="rounded-md border border-border bg-card/88 p-4">
<div className="font-display text-2xl font-semibold">{window.time}</div>
<div className="mt-1 text-sm font-semibold">{window.label}</div>
<div className="mt-1 text-xs leading-5 text-muted-foreground">{window.note}</div>
</div>
))}
</div>
</div>
<div className="grid gap-4 xl:grid-cols-[1fr_300px]">
<RestaurantPanel className="relative min-h-[620px] overflow-hidden bg-muted">
<Image src={site.heroImage} alt="Вечерняя посадка в Juniper Table" fill priority className="object-cover" sizes="(min-width: 1280px) 680px, 100vw" />
<div className="absolute bottom-5 left-5 right-5 rounded-md bg-background/92 p-5 shadow-xl backdrop-blur">
<div className="flex items-start justify-between gap-4">
<div>
<Badge variant="secondary" className="mb-3 rounded-md">tonight</Badge>
<h2 className="font-display text-3xl font-semibold">Chef counter: 4 места на 20:30</h2>
<p className="mt-2 text-sm leading-6 text-muted-foreground">Подходит для дегустационного сета и pairing.</p>
</div>
<div className="font-display text-2xl font-semibold">18</div>
</div>
</div>
</RestaurantPanel>
<div className="grid content-center gap-4">
{highlights.map((item) => {
const Icon = item.icon;
return (
<RestaurantPanel key={item.title} className="p-5">
<Icon className="mb-7 size-5 text-primary" />
<div className="font-display text-3xl font-semibold">{item.value}</div>
<h3 className="mt-2 font-semibold">{item.title}</h3>
<p className="mt-2 text-sm leading-6 text-muted-foreground">{item.text}</p>
</RestaurantPanel>
);
})}
</div>
</div>
</Container>
</section>
);
}
export function InnerHero({ eyebrow, title, text }: { eyebrow: string; title: string; text: string }) {
return (
<section className="border-b border-border bg-card">
<Container className="py-14">
<Badge variant="outline" className="mb-6 rounded-md border-primary/30 text-primary">{eyebrow}</Badge>
<h1 className="max-w-[980px] font-display text-4xl font-semibold leading-[1.02] sm:text-6xl">{title}</h1>
<p className="mt-5 max-w-[760px] text-base leading-7 text-muted-foreground">{text}</p>
</Container>
</section>
);
}
export function MenuSectionBoard() {
return (
<Container className="py-14">
<SectionHeader eyebrow="Menu architecture" title="Меню собрано по сценарию вечера" text="Гость видит не хаотичный список блюд, а путь: легкий старт, огонь и основные блюда, затем десерт и pairing." />
<div className="grid gap-4 md:grid-cols-3">
{menuSections.map((section) => (
<RestaurantPanel key={section.title} className="p-6">
<h3 className="font-display text-3xl font-semibold">{section.title}</h3>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{section.note}</p>
<Separator className="my-5" />
<div className="grid gap-2">
{section.items.map((item) => (
<div key={item} className="flex items-center gap-2 text-sm text-muted-foreground">
<CheckIcon className="size-4 text-primary" />
{item}
</div>
))}
</div>
</RestaurantPanel>
))}
</div>
</Container>
);
}
export function DishShowcase() {
const heroDish = dishes[1];
return (
<Container className="py-14">
<SectionHeader eyebrow="A la carte" title="Блюда продают вкус, ограничения и pairing сразу" text="Для ресторанного шаблона важно показывать аллергенные подсказки, секцию меню и сочетание напитков, а не только фото и цену." />
<div className="grid gap-4 lg:grid-cols-[1.05fr_0.95fr]">
<RestaurantPanel className="relative min-h-[560px] overflow-hidden bg-muted">
<Image src={heroDish.image} alt={heroDish.name} fill className="object-cover" sizes="(min-width: 1024px) 54vw, 100vw" />
<div className="absolute bottom-5 left-5 right-5 rounded-md bg-background/92 p-5 backdrop-blur">
<Badge variant="secondary" className="mb-3 rounded-md">{heroDish.tag}</Badge>
<div className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
<h3 className="font-display text-3xl font-semibold">{heroDish.name}</h3>
<div className="font-display text-3xl font-semibold">{heroDish.price}</div>
</div>
<p className="mt-2 text-sm text-muted-foreground">{heroDish.text}</p>
</div>
</RestaurantPanel>
<div className="grid gap-3">
{dishes.filter((dish) => dish.name !== heroDish.name).map((dish) => (
<DishRow key={dish.name} dish={dish} />
))}
</div>
</div>
</Container>
);
}
function DishRow({ dish }: { dish: DishItem }) {
return (
<article className="grid grid-cols-[132px_1fr] overflow-hidden rounded-md border border-border bg-card sm:grid-cols-[170px_1fr]">
<div className="relative min-h-[168px] bg-muted">
<Image src={dish.image} alt={dish.name} fill className="object-cover" sizes="180px" />
</div>
<div className="p-4">
<div className="mb-3 flex flex-wrap items-center gap-2">
<Badge variant="outline" className="rounded-md">{dish.section}</Badge>
<span className="text-sm font-semibold">{dish.price}</span>
</div>
<h3 className="font-display text-2xl font-semibold">{dish.name}</h3>
<p className="mt-2 text-sm leading-6 text-muted-foreground">{dish.text}</p>
<div className="mt-4 grid gap-2 text-xs text-muted-foreground sm:grid-cols-2">
<div>allergens: {dish.allergens}</div>
<div>pairing: {dish.pairing}</div>
</div>
</div>
</article>
);
}
export function FeaturedGrid({ eyebrow, title, text, items = dishes }: { eyebrow: string; title: string; text: string; items?: readonly DishItem[] }) {
return (
<Container className="py-14">
<SectionHeader eyebrow={eyebrow} title={title} text={text} />
<div className="overflow-hidden rounded-md border border-border bg-card">
{items.map((item) => (
<div key={item.name} className="grid gap-4 border-b border-border p-5 last:border-b-0 md:grid-cols-[1fr_140px_180px] md:items-center">
<div>
<div className="mb-2 text-xs uppercase tracking-[0.14em] text-primary">{item.tag}</div>
<h3 className="font-display text-2xl font-semibold">{item.name}</h3>
<p className="mt-2 text-sm leading-6 text-muted-foreground">{item.text}</p>
</div>
<div className="font-display text-2xl font-semibold md:text-right">{item.price}</div>
<div className="text-sm leading-6 text-muted-foreground">pairing: {item.pairing}</div>
</div>
))}
</div>
</Container>
);
}
export function MetricStrip() {
return (
<Container className="grid gap-3 py-8 md:grid-cols-3">
{highlights.map((item) => {
const Icon = item.icon;
return (
<RestaurantPanel key={item.title} className="p-5">
<Icon className="mb-6 size-5 text-primary" />
<div className="font-display text-4xl font-semibold">{item.value}</div>
<h3 className="mt-2 font-semibold">{item.title}</h3>
<p className="mt-2 text-sm leading-6 text-muted-foreground">{item.text}</p>
</RestaurantPanel>
);
})}
</Container>
);
}
export function PairingBoard() {
return (
<Container className="py-14">
<SectionHeader eyebrow="Pairing" title="Напитки продают темп вечера" text="Pairing вынесен отдельным блоком: вино, non-alcohol и спокойные альтернативы без давления на гостя." />
<div className="grid gap-4 md:grid-cols-3">
{pairingFlights.map((flight) => (
<RestaurantPanel key={flight.title} className="p-6">
<WineIcon className="mb-8 size-6 text-primary" />
<h3 className="font-display text-3xl font-semibold">{flight.title}</h3>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{flight.text}</p>
<div className="mt-5 font-display text-2xl font-semibold">{flight.price}</div>
</RestaurantPanel>
))}
</div>
</Container>
);
}
function WineIcon({ className }: { className?: string }) {
return <StarIcon className={className} />;
}
export function PricingTiles({ title, items = tastingSets }: { title: string; items?: readonly TileItem[] }) {
return (
<Container className="py-14">
<h2 className="mb-7 font-display text-4xl font-semibold sm:text-5xl">{title}</h2>
<div className="grid gap-4 md:grid-cols-3">
{items.map((item) => (
<RestaurantPanel key={item.title} className="p-6">
<h3 className="font-display text-2xl font-semibold">{item.title}</h3>
<div className="mt-3 font-display text-3xl font-semibold text-primary">{item.price}</div>
<Separator className="my-5" />
<div className="grid gap-2">
{item.items.map((line) => (
<div key={line} className="flex items-center gap-2 text-sm text-muted-foreground">
<CheckIcon className="size-4 text-primary" />
{line}
</div>
))}
</div>
</RestaurantPanel>
))}
</div>
</Container>
);
}
export function SeatingGuide() {
return (
<Container className="py-14">
<SectionHeader eyebrow="Seating" title="Посадка - это отдельный продукт ресторана" text="Гость выбирает не просто время, а формат: зал, chef counter или приватная комната с депозитом и ожиданиями." />
<div className="grid gap-4 md:grid-cols-3">
{seatingOptions.map((option) => (
<RestaurantPanel key={option.title} className="p-6">
<h3 className="font-display text-3xl font-semibold">{option.title}</h3>
<div className="mt-3 font-display text-2xl font-semibold text-primary">{option.seats}</div>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{option.text}</p>
<div className="mt-5 rounded-md bg-muted px-3 py-2 text-sm text-muted-foreground">{option.deposit}</div>
</RestaurantPanel>
))}
</div>
</Container>
);
}
export function RestaurantEveningPlan() {
return (
<Container className="py-14">
<div className="rounded-md bg-primary p-6 text-primary-foreground sm:p-8">
<h2 className="font-display text-4xl font-semibold">Маршрут вечера</h2>
<div className="mt-6 grid gap-3 md:grid-cols-3">
{eveningTimeline.map((step) => (
<div key={step.time} className="rounded-md border border-primary-foreground/18 p-5">
<div className="font-display text-3xl font-semibold">{step.time}</div>
<h3 className="mt-5 font-semibold">{step.title}</h3>
<p className="mt-2 text-sm leading-6 text-primary-foreground/72">{step.text}</p>
</div>
))}
</div>
</div>
</Container>
);
}
export function ReservationForm() {
return (
<Container className="grid gap-8 py-14 lg:grid-cols-[0.82fr_1.18fr]">
<div>
<Badge variant="outline" className="mb-5 rounded-md">
<CalendarDaysIcon className="size-4" />
booking flow
</Badge>
<h2 className="font-display text-4xl font-semibold sm:text-5xl">Бронь уточняет сценарий вечера</h2>
<p className="mt-4 text-sm leading-7 text-muted-foreground">
Форма статическая, но показывает production-ready сценарий: дата, время, гости, посадка, аллергии, событие и пожелания к темпу.
</p>
</div>
<form className="grid gap-4 rounded-md border border-border bg-card p-5">
<div className="grid gap-3 sm:grid-cols-2">
<Input placeholder="Имя" />
<Input placeholder="Телефон" />
<Input placeholder="Дата" />
<Input placeholder="Время" />
<Input placeholder="Количество гостей" />
<Input placeholder="Формат посадки" />
</div>
<div className="flex flex-wrap gap-2">
{["chef counter", "dining room", "private room", "tasting", "birthday"].map((tag) => (
<button key={tag} type="button" className="rounded-md border border-border bg-background px-3 py-2 text-sm font-medium text-muted-foreground">
{tag}
</button>
))}
</div>
<Textarea placeholder="Аллергии, dietary requests, детский стул, событие или пожелания к темпу вечера" />
<Button type="button" className="h-11 rounded-md">Отправить заявку хосту</Button>
</form>
</Container>
);
}
export function ReservationRules() {
return <InfoColumns title="Правила бронирования" items={bookingRules} />;
}
export function DietaryNotes() {
return <InfoColumns title="Allergies and dietary notes" items={dietaryNotes} />;
}
export function PrivateEventsGrid() {
return (
<Container className="py-14">
<SectionHeader eyebrow="Private dining" title="События продаются пакетами, а не одной формой обратной связи" text="Для ресторана private events - отдельная выручка: гости, формат, депозит, меню и тайминг должны быть видны заранее." />
<div className="grid gap-4 md:grid-cols-3">
{privatePackages.map((item) => (
<RestaurantPanel key={item.title} className="p-6">
<h3 className="font-display text-3xl font-semibold">{item.title}</h3>
<div className="mt-3 text-sm font-semibold text-primary">{item.guests}</div>
<div className="mt-5 font-display text-3xl font-semibold">{item.price}</div>
<Separator className="my-5" />
<div className="grid gap-2">
{item.details.map((detail) => (
<div key={detail} className="flex items-center gap-2 text-sm text-muted-foreground">
<CheckIcon className="size-4 text-primary" />
{detail}
</div>
))}
</div>
</RestaurantPanel>
))}
</div>
</Container>
);
}
export function EventPlanner() {
return (
<Container className="grid gap-4 py-12 lg:grid-cols-[1fr_1.05fr]">
<RestaurantPanel className="relative min-h-[460px] overflow-hidden bg-muted">
<Image src="https://images.unsplash.com/photo-1464366400600-7168b8af9bc3?auto=format&fit=crop&w=1200&q=82" alt="Private dining room" fill className="object-cover" sizes="(min-width: 1024px) 50vw, 100vw" />
</RestaurantPanel>
<div className="grid gap-4">
{eventTypes.map((item) => {
const Icon = item.icon;
return (
<RestaurantPanel key={item.title} className="p-6">
<Icon className="mb-7 size-6 text-primary" />
<h3 className="font-display text-3xl font-semibold">{item.title}</h3>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
</RestaurantPanel>
);
})}
</div>
</Container>
);
}
export function IconCards({ items }: { items: readonly IconItem[] }) {
return (
<Container className="grid gap-4 py-12 md:grid-cols-3">
{items.map((item) => {
const Icon = item.icon;
return (
<RestaurantPanel key={item.title} className="p-6">
<Icon className="mb-8 size-6 text-primary" />
<h3 className="font-display text-2xl font-semibold">{item.title}</h3>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
</RestaurantPanel>
);
})}
</Container>
);
}
export function ChefStory() {
return (
<Container className="grid gap-10 py-14 lg:grid-cols-2">
<RestaurantPanel className="relative min-h-[520px] overflow-hidden bg-muted">
<Image src="https://images.unsplash.com/photo-1577219491135-ce391730fb2c?auto=format&fit=crop&w=1200&q=82" alt="Kitchen team" fill className="object-cover" sizes="(min-width: 1024px) 50vw, 100vw" />
</RestaurantPanel>
<div className="flex flex-col justify-center">
<div className="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-primary">Kitchen story</div>
<h2 className="font-display text-4xl font-semibold leading-tight sm:text-5xl">Точная кухня и тихая уверенность сервиса</h2>
<p className="mt-5 text-base leading-7 text-muted-foreground">
Juniper Table строит меню вокруг короткого сезонного окна. Шеф-команда не обещает шоу: она держит продукт, соус, свет и темп вечера.
</p>
<div className="mt-7 grid gap-3">
{["поставки под меню недели", "открытая кухня без театральности", "зал держит ритм, а не давит на гостя"].map((point) => (
<div key={point} className="flex items-center gap-3 text-sm font-semibold">
<CheckIcon className="size-4 text-primary" />
{point}
</div>
))}
</div>
</div>
</Container>
);
}
export function SupplierLedger() {
return (
<Container className="py-12">
<SectionHeader eyebrow="Suppliers" title="Поставщики объясняют сезонность меню" text="Ресторанный сайт становится убедительнее, когда показывает, откуда берутся продукты и почему меню меняется." />
<div className="overflow-hidden rounded-md border border-border bg-card">
{suppliers.map((supplier) => (
<div key={supplier.name} className="grid gap-3 border-b border-border p-5 last:border-b-0 md:grid-cols-[1fr_1fr_180px]">
<div className="font-display text-2xl font-semibold">{supplier.name}</div>
<div className="text-sm text-muted-foreground">{supplier.product}</div>
<div className="text-sm font-semibold text-primary md:text-right">{supplier.cadence}</div>
</div>
))}
</div>
</Container>
);
}
export function SplitStory({ image, eyebrow, title, text, points }: { image: string; eyebrow: string; title: string; text: string; points: readonly string[] }) {
return (
<Container className="grid gap-10 py-14 lg:grid-cols-2">
<RestaurantPanel className="relative min-h-[500px] overflow-hidden bg-muted">
<Image src={image} alt={title} fill className="object-cover" sizes="(min-width: 1024px) 50vw, 100vw" />
</RestaurantPanel>
<div className="flex flex-col justify-center">
<div className="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-primary">{eyebrow}</div>
<h2 className="font-display text-4xl font-semibold leading-tight sm:text-5xl">{title}</h2>
<p className="mt-5 text-base leading-7 text-muted-foreground">{text}</p>
<div className="mt-7 grid gap-3">
{points.map((point) => (
<div key={point} className="flex items-center gap-3 text-sm font-semibold">
<CheckIcon className="size-4 text-primary" />
{point}
</div>
))}
</div>
</div>
</Container>
);
}
export function TestimonialBand({ items = testimonials }: { items?: readonly TestimonialItem[] }) {
return (
<Container className="grid gap-4 py-12 md:grid-cols-2">
{items.map((item) => (
<blockquote key={item.name} className="rounded-md border border-border bg-card p-7">
<div className="text-sm font-semibold text-primary">{item.rating}</div>
<p className="mt-5 font-display text-3xl leading-tight">"{item.text}"</p>
<footer className="mt-5 text-sm text-muted-foreground">{item.name}</footer>
</blockquote>
))}
</Container>
);
}
export function InfoColumns({ title, items }: { title: string; items: readonly InfoItem[] }) {
return (
<Container className="py-12">
<h2 className="mb-6 font-display text-4xl font-semibold">{title}</h2>
<div className="grid gap-4 md:grid-cols-3">
{items.map((item) => (
<RestaurantPanel key={item.title} className="p-6">
<StarIcon className="mb-8 size-5 text-primary" />
<h3 className="font-display text-2xl font-semibold">{item.title}</h3>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
</RestaurantPanel>
))}
</div>
</Container>
);
}
export function HoursBoard() {
return (
<Container className="py-12">
<div className="grid gap-4 md:grid-cols-3">
{serviceWindows.map((item) => (
<RestaurantPanel key={item.label} className="p-6">
<ClockIcon className="mb-8 size-6 text-primary" />
<h3 className="font-display text-3xl font-semibold">{item.label}</h3>
<div className="mt-3 font-display text-3xl font-semibold text-primary">{item.time}</div>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.note}</p>
</RestaurantPanel>
))}
</div>
</Container>
);
}
export function ContactForm() {
return (
<Container className="grid gap-6 py-14 lg:grid-cols-2">
<RestaurantPanel className="p-6">
<ClockIcon className="mb-8 size-6 text-primary" />
<h2 className="font-display text-4xl font-semibold">Связаться с хостом</h2>
<p className="mt-3 text-sm leading-7 text-muted-foreground">
Контактная страница закрывает практику: как добраться, когда работает кухня, как подтвердить бронь и где указать ограничения.
</p>
</RestaurantPanel>
<form className="grid gap-3 rounded-md border border-border bg-card p-5">
<Input placeholder="Ваше имя" />
<Input placeholder="Email или телефон" />
<Textarea placeholder="Дата, гости, событие, аллергии или вопрос хосту" />
<Button type="button" className="h-11 rounded-md">Отправить</Button>
</form>
</Container>
);
}
export function CtaPanel({ title, text, href, label }: { title: string; text: string; href: string; label: string }) {
return (
<Container className="py-12">
<div className="rounded-md bg-primary p-8 text-primary-foreground">
<h2 className="font-display text-4xl font-semibold">{title}</h2>
<p className="mt-4 max-w-[720px] text-sm leading-7 text-primary-foreground/72">{text}</p>
<Button asChild variant="secondary" className="mt-6 rounded-md">
<Link href={href}>{label}<ArrowRightIcon className="size-4" /></Link>
</Button>
</div>
</Container>
);
}

View File

@@ -0,0 +1,18 @@
import { testimonials } from "@/entities/site-content";
import { Container } from "@/shared/ui/container";
type TestimonialItem = { name: string; text: string; rating: string };
export function TestimonialBand({ items = testimonials }: { items?: readonly TestimonialItem[] }) {
return (
<Container className="grid gap-4 py-12 md:grid-cols-2">
{items.map((item) => (
<blockquote key={item.name} className="rounded-md border border-border bg-card p-7">
<div className="text-sm font-semibold text-primary">{item.rating}</div>
<p className="mt-5 font-display text-3xl leading-tight">"{item.text}"</p>
<footer className="mt-5 text-sm text-muted-foreground">{item.name}</footer>
</blockquote>
))}
</Container>
);
}