feat: split big file and update agents.md

This commit is contained in:
2026-06-18 23:14:26 +03:00
parent 0ab34ce1bc
commit 520cb0e184
23 changed files with 361 additions and 255 deletions

View File

@@ -5,7 +5,7 @@
## Project Specifics
- 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` — только route wrappers; композиция каждой страницы живёт в отдельном widget (`src/widgets/<page>-page.tsx`). Header/footer — в `src/widgets/site-shell.tsx`. См. File Map.
- Keep cards at 8px radius or less and preserve the selected palette in `src/app/globals.css`.
- This is a static frontend template: do not add real payments, auth, persistence, external API calls, or backend contracts without an explicit product request.
- After changes run `pnpm lint` and `pnpm build`.
@@ -14,3 +14,56 @@
- Кириллица обязательна для видимого текста и выбранных Google Fonts.
- При AI-правках сохраняйте доменные блоки этого шаблона: они отличают проект от generic landing.
## Design System
Источник токенов — `src/app/globals.css` (`@theme inline` + `:root`/`.dark`). Шрифты: заголовки и `.font-display`**Montserrat** (`--font-display`), body — **Inter** (`--font-sans`); mono замаплен на тот же display. Работай через семантические классы Tailwind (`bg-foreground`, `text-muted-foreground`, `border-border`), не хардкодь hex/oklch.
Личность: **premium monochrome fashion** — почти бесцветная ахроматическая палитра (всё в нейтральных серых, `chroma ≈ 0`), near-black «ink» как контраст, и единственный тёплый янтарный `accent` (oklch 0.72 0.16 71) для редких акцентов. Фотографии сознательно `grayscale` и расцвечиваются (`group-hover:grayscale-0`) только при наведении — это ключевой приём бренда.
| Роль | Light | Характер |
|---|---|---|
| `background` | почти белый neutral (0.985) | основной фон страниц |
| `foreground` | near-black ink (0.13) | текст + инверсный CTA/footer (`bg-foreground text-background`) |
| `primary` | near-black ink | логотип-плашка, primary-кнопки |
| `secondary` | светло-серый (0.94) | бейджи, теги товаров |
| `accent` | тёплый янтарь | редкий точечный акцент (по умолчанию почти не используется) |
| `muted` | серый (0.955 / fg 0.48) | фон image-плейсхолдеров, вторичный текст |
| `card` | чистый белый (1.0) | карточки на фоне 0.985 |
| `border` | мягкий серый (0.86) | тонкие границы (`border border-border`, НЕ толстые) |
Узнаваемые приёмы (держи их, это и есть «лицо» проекта):
- **Мягкие углы:** `--radius` = 0.375rem; почти всё — `rounded-md`. Никаких острых или сильно скруглённых форм.
- **Тонкие границы:** `border border-border` (1px), не толстые brutalist-рамки.
- **Grayscale → color на hover:** фото идут `object-cover grayscale`, при наведении карточки — `transition group-hover:grayscale-0` (+ лёгкий `group-hover:scale-[1.03]`).
- **Hover-lift с мягкой тенью:** `hover:-translate-y-1 hover:shadow-[0_24px_60px_rgba(0,0,0,0.09)]` — приподнятая карточка вместо жёсткого offset-shadow.
- **Типографика:** `font-semibold` (не black), очень крупные плотные заголовки (`text-7xl`+, `leading-[0.94]`), eyebrow — `uppercase tracking-[0.16em] text-muted-foreground`.
- **Инверсные блоки:** акцентные секции (footer, CTA, средняя карточка lookbook) — `bg-foreground text-background` с `text-background/70` для вторичного текста.
- **Фон-фактура:** `.fashion-fabric` — тонкая тканевая сетка для hero; фиксированная max-ширина контента `max-w-[1320px]`.
Do / Don't:
- **Do:** держи ахроматическую палитру, тонкие границы, grayscale-фото, product-first сетки, крупную сдержанную типографику.
- **Don't:** яркие цвета/градиенты, толстые рамки, жёсткие offset-тени, `rounded-none` или `rounded-full` на карточках, `font-black` — это ломает premium-минимализм.
## File Map
| Route | Widget |
|---|---|
| `/` | `src/widgets/home-page.tsx` (`HomePage`) |
| `/catalog` | `src/widgets/catalog-page.tsx` (`CatalogPage`) |
| `/product/classic-overshirt` | `src/widgets/product-detail-page.tsx` (`ProductDetailPage`) |
| `/lookbook` | `src/widgets/lookbook-page.tsx` (`LookbookPage`) |
| `/cart` | `src/widgets/cart-page.tsx` (`CartPage`) |
| `/shipping` | `src/widgets/shipping-page.tsx` (`ShippingPage`) |
Переиспользуемые блоки:
- `src/widgets/site-shell.tsx``SiteHeader` + `SiteFooter` (обёртка всех страниц через `app/layout.tsx`).
- `src/widgets/featured-grid.tsx``FeaturedGrid` (Home, Catalog).
- `src/widgets/collection-board.tsx``CollectionBoard` (Home, Lookbook).
- `src/widgets/product-assurance.tsx``ProductAssurance` (Home, Product).
- `src/shared/ui/inner-hero.tsx``InnerHero` (заголовочная секция внутренних страниц).
- `src/shared/ui/icon-cards.tsx``IconCards` (Shipping, Product).
- `src/shared/ui/info-columns.tsx``InfoColumns` (Shipping, Cart).
- `src/shared/ui/split-story.tsx``SplitStory` (Home, Lookbook).
Одноразовые блоки колоцированы со своей страницей: `PageHero`/`MetricStrip`/`TestimonialBand` в `home-page.tsx`, `CatalogToolbar`/`PricingTiles` в `catalog-page.tsx`, `LookbookGrid` в `lookbook-page.tsx`, `CartPreview` в `cart-page.tsx`, `ProductDetail`/`OptionRow`/`CtaPanel` в `product-detail-page.tsx`.

View File

@@ -1,14 +1,5 @@
"use client";
import { CartPreview, InfoColumns, InnerHero } from "@/widgets/template-ui";
import { CartPage } from "@/widgets/cart-page";
export default function Page() {
return (
<>
<InnerHero eyebrow="Cart" title="Корзина с понятным итогом, доставкой и add-ons" text="Статический checkout preview без платежей и API, готовый для дальнейшей интеграции." />
<CartPreview />
<InfoColumns title="Перед оплатой" items={[{ title: "Промокод", text: "Место под promo state и пересчет total." }, { title: "Доставка", text: "Estimator показывает сроки и условия." }, { title: "Add-ons", text: "Рекомендации повышают средний чек." }]} />
</>
);
return <CartPage />;
}

View File

@@ -1,16 +1,5 @@
"use client";
import { CatalogToolbar, FeaturedGrid, InnerHero, PricingTiles } from "@/widgets/template-ui";
import { products, tastingSets } from "@/entities/site-content";
import { CatalogPage } from "@/widgets/catalog-page";
export default function Page() {
return (
<>
<InnerHero eyebrow="Catalog" title="Коллекция core pieces с фильтрами и быстрым сканированием" text="Страница показывает ecommerce-сценарий: карточки, теги, цены, категории и trust-сообщения." />
<CatalogToolbar />
<FeaturedGrid eyebrow="All products" title="Drop 01" text="Минимальный каталог с product cards, который легко расширить реальными данными." items={products} />
<PricingTiles title="Готовые комплекты" items={tastingSets} />
</>
);
return <CatalogPage />;
}

View File

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

View File

@@ -1,15 +1,5 @@
"use client";
import { CollectionBoard, InnerHero, LookbookGrid, SplitStory } from "@/widgets/template-ui";
import { LookbookPage } from "@/widgets/lookbook-page";
export default function Page() {
return (
<>
<InnerHero eyebrow="Lookbook" title="Кампания сезона: черный, молочный, сталь и движение" text="Editorial-страница для визуального продвижения коллекций, drop stories и moodboard." />
<CollectionBoard />
<LookbookGrid />
<SplitStory image="https://images.unsplash.com/photo-1496747611176-843222e1e57c?auto=format&fit=crop&w=1100&q=80" eyebrow="Campaign" title="Показывайте одежду в реальном сценарии" text="Lookbook раскрывает посадку и сочетания лучше, чем обычная карточка товара." points={["4 hero looks", "mobile-first grid", "CTA в каталог"]} />
</>
);
return <LookbookPage />;
}

View File

@@ -1,28 +1,5 @@
"use client";
import { CollectionBoard, FeaturedGrid, MetricStrip, PageHero, ProductAssurance, SplitStory, TestimonialBand } from "@/widgets/template-ui";
import { highlights, products, site, testimonials } from "@/entities/site-content";
import { HomePage } from "@/widgets/home-page";
export default function Page() {
return (
<>
<PageHero
eyebrow="Core collection 2026"
title="Одежда, которая не спорит с вашим днем"
text={site.tagline}
primaryHref="/catalog"
primaryLabel={site.cta}
secondaryHref="/lookbook"
secondaryLabel={site.secondaryCta}
image={site.heroImage}
/>
<CollectionBoard />
<MetricStrip items={highlights} />
<FeaturedGrid eyebrow="Featured products" title="Капсула без случайных вещей" text="Каталог построен так, чтобы быстро заменить продукты, цены и фильтры под реальный магазин." items={products} />
<SplitStory image={site.accentImage} eyebrow="Lookbook" title="Editorial grid вместо шумного маркетплейса" text="Образы продают посадку, материал и настроение коллекции, не превращая магазин в баннерную сетку." points={["Черный как основа", "Фактура крупным планом", "Готовые комплекты"]} />
<ProductAssurance />
<TestimonialBand items={testimonials} />
</>
);
return <HomePage />;
}

View File

@@ -1,16 +1,5 @@
"use client";
import { CtaPanel, IconCards, ProductAssurance, ProductDetail } from "@/widgets/template-ui";
import { eventTypes } from "@/entities/site-content";
import { ProductDetailPage } from "@/widgets/product-detail-page";
export default function Page() {
return (
<>
<ProductDetail />
<ProductAssurance />
<IconCards items={eventTypes} />
<CtaPanel title="Не уверены в размере?" text="Откройте size help или добавьте товар в wishlist, чтобы вернуться позже." href="/shipping" label="Доставка и размеры" />
</>
);
return <ProductDetailPage />;
}

View File

@@ -1,15 +1,5 @@
"use client";
import { IconCards, InfoColumns, InnerHero } from "@/widgets/template-ui";
import { contactCards } from "@/entities/site-content";
import { ShippingPage } from "@/widgets/shipping-page";
export default function Page() {
return (
<>
<InnerHero eyebrow="Shipping" title="Доставка, возвраты, размер и уход" text="Сервисная страница снижает нагрузку на поддержку и повышает доверие к покупке." />
<IconCards items={contactCards} />
<InfoColumns title="FAQ" items={[{ title: "Как вернуть заказ?", text: "Заполните форму возврата в течение 30 дней." }, { title: "Что с размером?", text: "На PDP есть замеры вещи и рекомендации по посадке." }, { title: "Как ухаживать?", text: "Рекомендации по уходу указаны в карточке и на бирке." }]} />
</>
);
return <ShippingPage />;
}

View File

@@ -0,0 +1,8 @@
import { eventTypes } from "@/entities/site-content";
type IconComponent = React.ComponentType<{ className?: string }>;
type IconItem = { title: string; text: string; icon: IconComponent };
export function IconCards({ items = eventTypes }: { items?: readonly IconItem[] }) {
return <section className="mx-auto grid w-full max-w-[1320px] gap-4 px-5 py-12 sm:px-8 md:grid-cols-3">{items.map((item) => { const Icon = item.icon; return <article key={item.title} className="rounded-md border border-border bg-card p-6"><Icon className="mb-8 size-5" /><h3 className="text-xl font-semibold">{item.title}</h3><p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p></article>; })}</section>;
}

View File

@@ -0,0 +1,3 @@
export function InfoColumns({ title, items }: { title: string; items: readonly { title: string; text: string }[] }) {
return <section className="mx-auto w-full max-w-[1320px] px-5 py-12 sm:px-8"><h2 className="mb-6 text-4xl font-semibold">{title}</h2><div className="grid gap-4 md:grid-cols-3">{items.map((item) => <article key={item.title} className="rounded-md border border-border bg-card p-6"><h3 className="text-xl font-semibold">{item.title}</h3><p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p></article>)}</div></section>;
}

View File

@@ -0,0 +1,5 @@
import { Badge } from "@/shared/ui/badge";
export function InnerHero({ eyebrow, title, text }: { eyebrow: string; title: string; text: string }) {
return <section className="border-b border-border bg-card"><div className="mx-auto w-full max-w-[1320px] px-5 py-14 sm:px-8"><Badge variant="outline" className="mb-6 rounded-md border-foreground/20">{eyebrow}</Badge><h1 className="max-w-[980px] text-5xl font-semibold leading-[0.98] tracking-normal sm:text-7xl">{title}</h1><p className="mt-6 max-w-[720px] text-base leading-7 text-muted-foreground sm:text-lg">{text}</p></div></section>;
}

View File

@@ -0,0 +1,6 @@
import Image from "next/image";
import { CheckIcon } from "lucide-react";
export function SplitStory({ image, eyebrow, title, text, points }: { image: string; eyebrow: string; title: string; text: string; points: readonly string[] }) {
return <section className="mx-auto grid w-full max-w-[1320px] gap-10 px-5 py-14 sm:px-8 lg:grid-cols-2"><div className="relative min-h-[520px] overflow-hidden rounded-md bg-muted"><Image src={image} alt={title} fill className="object-cover grayscale" sizes="(min-width: 1024px) 50vw, 100vw" /></div><div className="flex flex-col justify-center"><div className="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">{eyebrow}</div><h2 className="text-4xl font-semibold leading-tight sm:text-6xl">{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" />{point}</div>)}</div></div></section>;
}

24
src/widgets/cart-page.tsx Normal file
View File

@@ -0,0 +1,24 @@
"use client";
import Image from "next/image";
import { MinusIcon, PlusIcon } from "lucide-react";
import { products } from "@/entities/site-content";
import { Button } from "@/shared/ui/button";
import { Separator } from "@/shared/ui/separator";
import { InfoColumns } from "@/shared/ui/info-columns";
import { InnerHero } from "@/shared/ui/inner-hero";
function CartPreview() {
return <section className="mx-auto grid w-full max-w-[1320px] gap-8 px-5 py-12 sm:px-8 lg:grid-cols-[1fr_380px]"><div className="grid gap-4">{products.slice(0, 3).map((item, index) => <article key={item.name} className="grid grid-cols-[112px_1fr_auto] items-center gap-5 rounded-md border border-border bg-card p-4"><div className="relative aspect-square overflow-hidden rounded-md bg-muted"><Image src={item.image} alt={item.name} fill className="object-cover grayscale" sizes="120px" /></div><div><h3 className="font-semibold">{item.name}</h3><p className="mt-1 text-sm text-muted-foreground">{index === 0 ? "black / L" : "black / M"}</p></div><div className="flex items-center gap-3"><Button variant="outline" size="icon" className="rounded-md"><MinusIcon className="size-4" /></Button><span className="font-semibold">1</span><Button variant="outline" size="icon" className="rounded-md"><PlusIcon className="size-4" /></Button></div></article>)}</div><aside className="h-fit rounded-md border border-border bg-card p-6"><h2 className="text-2xl font-semibold">Итог</h2><div className="mt-6 grid gap-3 text-sm"><div className="flex justify-between"><span>Товары</span><span>42 100 </span></div><div className="flex justify-between"><span>Доставка</span><span>0 </span></div><Separator /><div className="flex justify-between text-lg font-semibold"><span>К оплате</span><span>42 100 </span></div></div><Button className="mt-6 h-12 w-full rounded-md bg-foreground text-background hover:bg-foreground/90">Оформить заказ</Button></aside></section>;
}
export function CartPage() {
return (
<>
<InnerHero eyebrow="Cart" title="Корзина с понятным итогом, доставкой и add-ons" text="Статический checkout preview без платежей и API, готовый для дальнейшей интеграции." />
<CartPreview />
<InfoColumns title="Перед оплатой" items={[{ title: "Промокод", text: "Место под promo state и пересчет total." }, { title: "Доставка", text: "Estimator показывает сроки и условия." }, { title: "Add-ons", text: "Рекомендации повышают средний чек." }]} />
</>
);
}

View File

@@ -0,0 +1,31 @@
"use client";
import { CheckIcon, FilterIcon, SearchIcon, SlidersHorizontalIcon } from "lucide-react";
import { products, tastingSets } from "@/entities/site-content";
import { Button } from "@/shared/ui/button";
import { Input } from "@/shared/ui/input";
import { Separator } from "@/shared/ui/separator";
import { FeaturedGrid } from "@/widgets/featured-grid";
import { InnerHero } from "@/shared/ui/inner-hero";
type TileItem = { title: string; price: string; items: readonly string[] };
function CatalogToolbar() {
return <section className="mx-auto flex w-full max-w-[1320px] flex-col gap-3 px-5 py-6 sm:px-8 md:flex-row"><div className="flex min-h-12 flex-1 items-center gap-3 rounded-md border border-border bg-card px-4"><SearchIcon className="size-4 text-muted-foreground" /><Input className="border-0 bg-transparent shadow-none focus-visible:ring-0" placeholder="Поиск по модели, ткани или размеру" /></div><Button variant="outline" className="h-12 rounded-md"><FilterIcon className="size-4" /> Фильтр</Button><Button variant="outline" className="h-12 rounded-md"><SlidersHorizontalIcon className="size-4" /> Сортировка</Button></section>;
}
function PricingTiles({ title, items }: { title: string; items: readonly TileItem[] }) {
return <section className="mx-auto w-full max-w-[1320px] px-5 py-14 sm:px-8"><h2 className="mb-7 text-4xl font-semibold sm:text-6xl">{title}</h2><div className="grid gap-4 md:grid-cols-3">{items.map((item) => <article key={item.title} className="rounded-md border border-border bg-card p-6"><h3 className="text-2xl font-semibold">{item.title}</h3><div className="mt-3 text-4xl font-semibold">{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-foreground" />{line}</div>)}</div></article>)}</div></section>;
}
export function CatalogPage() {
return (
<>
<InnerHero eyebrow="Catalog" title="Коллекция core pieces с фильтрами и быстрым сканированием" text="Страница показывает ecommerce-сценарий: карточки, теги, цены, категории и trust-сообщения." />
<CatalogToolbar />
<FeaturedGrid eyebrow="All products" title="Drop 01" text="Минимальный каталог с product cards, который легко расширить реальными данными." items={products} />
<PricingTiles title="Готовые комплекты" items={tastingSets} />
</>
);
}

View File

@@ -0,0 +1,13 @@
import Image from "next/image";
import Link from "next/link";
import { ArrowRightIcon } from "lucide-react";
import { products, site } from "@/entities/site-content";
import { Button } from "@/shared/ui/button";
const productImages = products.map((item) => item.image);
export function CollectionBoard() {
const looks = [{ title: "01 / Layered city", image: site.heroImage, text: "overshirt поверх плотного jersey" }, { title: "02 / Evening black", image: site.accentImage, text: "широкие брюки и структурный верх" }, { title: "03 / Travel shell", image: productImages[2], text: "сумка utility и легкий слой" }] as const;
return <section className="mx-auto w-full max-w-[1320px] px-5 py-16 sm:px-8"><div className="mb-9 flex flex-col gap-4 md:flex-row md:items-end md:justify-between"><h2 className="max-w-[760px] text-4xl font-semibold leading-tight sm:text-6xl">Lookbook говорит о посадке, ткани и комплекте</h2><Button asChild variant="outline" className="h-11 w-fit rounded-md"><Link href="/catalog">В каталог <ArrowRightIcon className="size-4" /></Link></Button></div><div className="grid gap-4 md:grid-cols-3">{looks.map((look, index) => <article key={look.title} className={index === 1 ? "overflow-hidden rounded-md bg-foreground text-background" : "overflow-hidden rounded-md border border-border bg-card"}><div className="relative min-h-[420px] bg-muted"><Image src={look.image} alt={look.title} fill className="object-cover grayscale" sizes="(min-width: 768px) 33vw, 100vw" /></div><div className="p-5"><h3 className="text-lg font-semibold">{look.title}</h3><p className={index === 1 ? "mt-2 text-sm text-background/70" : "mt-2 text-sm text-muted-foreground"}>{look.text}</p></div></article>)}</div></section>;
}

View File

@@ -0,0 +1,11 @@
import Image from "next/image";
import Link from "next/link";
import { site } from "@/entities/site-content";
import { Badge } from "@/shared/ui/badge";
type TextItem = { name: string; price: string; tag: string; text: string; image?: string };
export function FeaturedGrid({ eyebrow, title, text, items }: { eyebrow: string; title: string; text: string; items: readonly TextItem[] }) {
return <section className="mx-auto w-full max-w-[1320px] px-5 py-14 sm:px-8"><div className="mb-8 grid gap-5 md:grid-cols-[0.8fr_1fr]"><div><div className="mb-2 text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">{eyebrow}</div><h2 className="text-4xl font-semibold leading-tight tracking-normal sm:text-6xl">{title}</h2></div><p className="max-w-[560px] text-base leading-7 text-muted-foreground">{text}</p></div><div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">{items.map((item) => <Link key={item.name} href="/product/classic-overshirt" className="group overflow-hidden rounded-md border border-border bg-card transition hover:-translate-y-1 hover:shadow-[0_24px_60px_rgba(0,0,0,0.09)]"><div className="relative aspect-[4/5] bg-muted"><Image src={item.image ?? site.heroImage} alt={item.name} fill className="object-cover grayscale transition duration-500 group-hover:scale-[1.03] group-hover:grayscale-0" sizes="(min-width: 1024px) 25vw, 50vw" /></div><div className="p-4"><div className="mb-4 flex items-center justify-between gap-3"><Badge variant="secondary" className="rounded-md">{item.tag}</Badge><span className="text-sm font-semibold">{item.price}</span></div><h3 className="text-lg font-semibold leading-tight">{item.name}</h3><p className="mt-2 text-sm leading-6 text-muted-foreground">{item.text}</p></div></Link>)}</div></section>;
}

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

@@ -0,0 +1,71 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { ArrowRightIcon } from "lucide-react";
import { highlights, products, site, testimonials } from "@/entities/site-content";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { SplitStory } from "@/shared/ui/split-story";
import { CollectionBoard } from "@/widgets/collection-board";
import { FeaturedGrid } from "@/widgets/featured-grid";
import { ProductAssurance } from "@/widgets/product-assurance";
function PageHero() {
return (
<section className="relative overflow-hidden border-b border-border">
<div className="fashion-fabric absolute inset-0 opacity-70" />
<div className="relative mx-auto grid min-h-[720px] w-full max-w-[1320px] items-center gap-12 px-5 py-16 sm:px-8 lg:grid-cols-[0.78fr_1.22fr]">
<div className="max-w-[620px]">
<Badge variant="outline" className="mb-8 rounded-md border-foreground/20 bg-background/70">Core collection 2026</Badge>
<h1 className="text-5xl font-semibold leading-[0.94] tracking-normal sm:text-7xl lg:text-[6.6rem]">Гардероб, который держит форму дня</h1>
<p className="mt-8 max-w-[560px] text-lg leading-8 text-muted-foreground">{site.tagline}</p>
<div className="mt-9 flex flex-col gap-3 sm:flex-row">
<Button asChild className="h-12 rounded-md bg-foreground px-6 text-background hover:bg-foreground/90"><Link href="/catalog">Смотреть drop <ArrowRightIcon className="size-4" /></Link></Button>
<Button asChild variant="outline" className="h-12 rounded-md px-6"><Link href="/lookbook">Открыть lookbook</Link></Button>
</div>
</div>
<div className="grid gap-4 lg:grid-cols-[1fr_0.72fr]">
<div className="relative min-h-[610px] overflow-hidden rounded-md bg-muted">
<Image src={site.heroImage} alt="Модель в черной коллекции Monochrome Supply" fill priority className="object-cover grayscale" sizes="(min-width: 1024px) 620px, 100vw" />
<div className="absolute bottom-4 left-4 right-4 flex items-center justify-between rounded-md bg-background/92 p-4 shadow-xl backdrop-blur">
<div><div className="text-sm font-semibold">Классическая overshirt</div><div className="text-xs text-muted-foreground">cotton twill / black / прямой крой</div></div>
<div className="text-sm font-semibold">17 900 </div>
</div>
</div>
<div className="grid gap-4">
{products.slice(1, 4).map((item) => (
<Link href="/product/classic-overshirt" key={item.name} className="group grid grid-cols-[112px_1fr] overflow-hidden rounded-md border border-border bg-card transition hover:-translate-y-0.5 hover:shadow-[0_18px_45px_rgba(0,0,0,0.08)]">
<div className="relative min-h-[154px] bg-muted"><Image src={item.image} alt={item.name} fill className="object-cover grayscale transition group-hover:grayscale-0" sizes="160px" /></div>
<div className="flex flex-col justify-between p-4"><Badge variant="secondary" className="w-fit rounded-md">{item.tag}</Badge><div><div className="font-semibold leading-tight">{item.name}</div><div className="mt-2 text-sm text-muted-foreground">{item.price}</div></div></div>
</Link>
))}
</div>
</div>
</div>
</section>
);
}
function MetricStrip() {
return <section className="mx-auto grid w-full max-w-[1320px] gap-4 px-5 py-8 sm:px-8 md:grid-cols-3">{highlights.map((item) => { const Icon = item.icon; return <article key={item.title} className="rounded-md border border-border bg-card p-5"><Icon className="mb-8 size-5" /><div className="text-4xl font-semibold tracking-normal">{item.value}</div><h3 className="mt-3 font-semibold">{item.title}</h3><p className="mt-2 text-sm leading-6 text-muted-foreground">{item.text}</p></article>; })}</section>;
}
function TestimonialBand() {
return <section className="mx-auto grid w-full max-w-[1320px] gap-4 px-5 py-12 sm:px-8 md:grid-cols-2">{testimonials.map((item) => <blockquote key={item.name} className="rounded-md border border-border bg-card p-7"><div className="text-sm font-semibold">{item.rating}</div><p className="mt-5 text-2xl font-semibold leading-snug">{item.text}</p><footer className="mt-5 text-sm text-muted-foreground">{item.name}</footer></blockquote>)}</section>;
}
export function HomePage() {
return (
<>
<PageHero />
<CollectionBoard />
<MetricStrip />
<FeaturedGrid eyebrow="Featured products" title="Капсула без случайных вещей" text="Каталог построен так, чтобы быстро заменить продукты, цены и фильтры под реальный магазин." items={products} />
<SplitStory image={site.accentImage} eyebrow="Lookbook" title="Editorial grid вместо шумного маркетплейса" text="Образы продают посадку, материал и настроение коллекции, не превращая магазин в баннерную сетку." points={["Черный как основа", "Фактура крупным планом", "Готовые комплекты"]} />
<ProductAssurance />
<TestimonialBand />
</>
);
}

View File

@@ -0,0 +1,26 @@
"use client";
import Image from "next/image";
import { products, site } from "@/entities/site-content";
import { SplitStory } from "@/shared/ui/split-story";
import { CollectionBoard } from "@/widgets/collection-board";
import { InnerHero } from "@/shared/ui/inner-hero";
const productImages = products.map((item) => item.image);
function LookbookGrid() {
const images = [site.heroImage, site.accentImage, productImages[0], productImages[1], productImages[2], productImages[3]];
return <section className="mx-auto grid w-full max-w-[1320px] gap-4 px-5 py-12 sm:px-8 md:grid-cols-4">{images.map((image, index) => <div key={image} className={index === 0 || index === 5 ? "relative min-h-[520px] overflow-hidden rounded-md bg-muted md:col-span-2 md:row-span-2" : "relative min-h-[250px] overflow-hidden rounded-md bg-muted"}><Image src={image} alt="Lookbook Monochrome Supply" fill className="object-cover grayscale" sizes="(min-width: 768px) 25vw, 100vw" /></div>)}</section>;
}
export function LookbookPage() {
return (
<>
<InnerHero eyebrow="Lookbook" title="Кампания сезона: черный, молочный, сталь и движение" text="Editorial-страница для визуального продвижения коллекций, drop stories и moodboard." />
<CollectionBoard />
<LookbookGrid />
<SplitStory image="https://images.unsplash.com/photo-1496747611176-843222e1e57c?auto=format&fit=crop&w=1100&q=80" eyebrow="Campaign" title="Показывайте одежду в реальном сценарии" text="Lookbook раскрывает посадку и сочетания лучше, чем обычная карточка товара." points={["4 hero looks", "mobile-first grid", "CTA в каталог"]} />
</>
);
}

View File

@@ -0,0 +1,4 @@
export function ProductAssurance({ compact = false }: { compact?: boolean }) {
const rows = [["Материал", "Плотный хлопковый twill держит форму и не выглядит спортивно."], ["Посадка", "Прямой крой оставляет место под слой и не утяжеляет силуэт."], ["После покупки", "30 дней на возврат, примерочный интервал и подсказки по уходу."]] as const;
return <section className={compact ? "mt-8" : "mx-auto w-full max-w-[1320px] px-5 py-12 sm:px-8"}><div className="overflow-hidden rounded-md border border-border bg-card">{rows.map(([title, text]) => <div key={title} className="grid gap-2 border-b border-border p-4 last:border-b-0 sm:grid-cols-[150px_1fr]"><h3 className="font-semibold">{title}</h3><p className="text-sm leading-6 text-muted-foreground">{text}</p></div>)}</div></section>;
}

View File

@@ -0,0 +1,37 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { ArrowRightIcon, ChevronRightIcon, HeartIcon, ShoppingCartIcon } from "lucide-react";
import { eventTypes, products, site } from "@/entities/site-content";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { Separator } from "@/shared/ui/separator";
import { IconCards } from "@/shared/ui/icon-cards";
import { ProductAssurance } from "@/widgets/product-assurance";
const productImages = products.map((item) => item.image);
function OptionRow({ label, options, active }: { label: string; options: readonly string[]; active?: string }) {
return <div className="mb-6"><div className="mb-3 text-sm font-semibold">{label}</div><div className="flex flex-wrap gap-2">{options.map((option) => <Button key={option} variant={option === active ? "default" : "outline"} className="h-10 rounded-md px-4">{option}</Button>)}</div></div>;
}
function ProductDetail() {
return <section className="mx-auto grid w-full max-w-[1320px] gap-12 px-5 py-14 sm:px-8 lg:grid-cols-[1.04fr_0.96fr]"><div className="grid gap-4"><div className="relative min-h-[680px] overflow-hidden rounded-md bg-muted"><Image src={site.heroImage} alt="Классическая overshirt" fill priority className="object-cover grayscale" sizes="(min-width: 1024px) 620px, 100vw" /></div><div className="grid grid-cols-4 gap-3">{[site.heroImage, site.accentImage, productImages[1], productImages[2]].map((image) => <div key={image} className="relative aspect-square overflow-hidden rounded-md bg-muted"><Image src={image} alt="Деталь товара" fill className="object-cover grayscale" sizes="160px" /></div>)}</div></div><div className="py-4"><div className="mb-5 flex items-center gap-2 text-sm text-muted-foreground">Каталог <ChevronRightIcon className="size-4" /> Верхний слой <ChevronRightIcon className="size-4" /> Overshirt</div><h1 className="text-5xl font-semibold leading-[0.96] sm:text-7xl">Классическая overshirt</h1><div className="mt-6 flex items-center gap-3"><Badge variant="outline" className="rounded-md">4.8 </Badge><span className="text-sm text-muted-foreground">210 отзывов</span></div><div className="mt-9 flex flex-wrap items-end gap-4"><div className="text-5xl font-semibold">17 900 </div><div className="pb-1 text-sm text-muted-foreground line-through">22 900 </div><Badge className="rounded-md bg-secondary text-secondary-foreground hover:bg-secondary">Скидка 20%</Badge></div><p className="mt-7 text-lg leading-8 text-muted-foreground">Плотный cotton twill, прямой крой, скрытая планка и аккуратный вес для межсезонья.</p><Separator className="my-8" /><OptionRow label="Цвет" options={["черный", "графит", "молочный", "шалфей"]} /><OptionRow label="Размер" options={["XS", "S", "M", "L", "XL"]} active="L" /><div className="mt-8 grid gap-3 sm:grid-cols-2"><Button className="h-12 rounded-md bg-foreground text-background hover:bg-foreground/90"><ShoppingCartIcon className="size-4" /> В корзину</Button><Button variant="outline" className="h-12 rounded-md"><HeartIcon className="size-4" /> В избранное</Button></div><ProductAssurance compact /></div></section>;
}
function CtaPanel({ title, text, href, label }: { title: string; text: string; href: string; label: string }) {
return <section className="mx-auto w-full max-w-[1320px] px-5 py-12 sm:px-8"><div className="rounded-md bg-foreground p-8 text-background"><h2 className="text-4xl font-semibold">{title}</h2><p className="mt-4 max-w-[720px] text-sm leading-7 text-background/70">{text}</p><Button asChild variant="secondary" className="mt-6 rounded-md"><Link href={href}>{label}<ArrowRightIcon className="size-4" /></Link></Button></div></section>;
}
export function ProductDetailPage() {
return (
<>
<ProductDetail />
<ProductAssurance />
<IconCards items={eventTypes} />
<CtaPanel title="Не уверены в размере?" text="Откройте size help или добавьте товар в wishlist, чтобы вернуться позже." href="/shipping" label="Доставка и размеры" />
</>
);
}

View File

@@ -0,0 +1,16 @@
"use client";
import { contactCards } from "@/entities/site-content";
import { IconCards } from "@/shared/ui/icon-cards";
import { InfoColumns } from "@/shared/ui/info-columns";
import { InnerHero } from "@/shared/ui/inner-hero";
export function ShippingPage() {
return (
<>
<InnerHero eyebrow="Shipping" title="Доставка, возвраты, размер и уход" text="Сервисная страница снижает нагрузку на поддержку и повышает доверие к покупке." />
<IconCards items={contactCards} />
<InfoColumns title="FAQ" items={[{ title: "Как вернуть заказ?", text: "Заполните форму возврата в течение 30 дней." }, { title: "Что с размером?", text: "На PDP есть замеры вещи и рекомендации по посадке." }, { title: "Как ухаживать?", text: "Рекомендации по уходу указаны в карточке и на бирке." }]} />
</>
);
}

View File

@@ -0,0 +1,39 @@
import Link from "next/link";
import { MenuIcon } from "lucide-react";
import { site } from "@/entities/site-content";
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-4">
<span className="flex size-9 items-center justify-center rounded-md bg-primary text-sm font-bold text-primary-foreground">Mo</span>
<span className="text-xl font-semibold tracking-normal">{site.name}</span>
</Link>
<nav className="hidden items-center gap-8 md:flex">
{site.nav.map((item) => (
<Link key={item.href} href={item.href} className="text-sm font-semibold text-muted-foreground transition hover:text-foreground">{item.label}</Link>
))}
</nav>
<div className="flex items-center gap-2">
<Button asChild className="hidden h-11 rounded-md bg-foreground px-6 text-background hover:bg-foreground/88 sm:inline-flex"><Link href="/catalog">{site.cta}</Link></Button>
<Button variant="outline" size="icon" className="rounded-md md:hidden" aria-label="Открыть меню"><MenuIcon className="size-4" /></Button>
</div>
</div>
</header>
);
}
export function SiteFooter() {
return (
<footer className="border-t border-border bg-foreground text-background">
<div className="mx-auto grid w-full max-w-[1320px] gap-10 px-5 py-12 sm:px-8 md:grid-cols-[1.4fr_1fr_1fr]">
<div><div className="text-2xl font-semibold">{site.name}</div><p className="mt-4 max-w-[420px] text-sm leading-6 text-background/64">{site.tagline}</p></div>
<div className="grid gap-2 text-sm text-background/70"><div className="mb-2 font-semibold text-background">Коллекция</div>{site.nav.slice(0, 4).map((item) => <Link key={item.href} href={item.href}>{item.label}</Link>)}</div>
<div className="text-sm leading-6 text-background/70"><div className="mb-2 font-semibold text-background">Шаблон</div>Product-first ecommerce с каталогом, PDP, lookbook, корзиной и сервисной страницей.</div>
</div>
</footer>
);
}

View File

@@ -1,167 +0,0 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import {
ArrowRightIcon,
CheckIcon,
ChevronRightIcon,
FilterIcon,
HeartIcon,
MenuIcon,
MinusIcon,
PlusIcon,
SearchIcon,
ShoppingCartIcon,
SlidersHorizontalIcon,
} from "lucide-react";
import { eventTypes, highlights, products, site, 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";
type IconComponent = React.ComponentType<{ className?: string }>;
type TextItem = { name: string; price: string; tag: string; text: string; image?: string };
type TileItem = { title: string; price: string; items: readonly string[] };
type IconItem = { title: string; text: string; icon: IconComponent };
const productImages = products.map((item) => item.image);
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-4">
<span className="flex size-9 items-center justify-center rounded-md bg-primary text-sm font-bold text-primary-foreground">Mo</span>
<span className="text-xl font-semibold tracking-normal">{site.name}</span>
</Link>
<nav className="hidden items-center gap-8 md:flex">
{site.nav.map((item) => (
<Link key={item.href} href={item.href} className="text-sm font-semibold text-muted-foreground transition hover:text-foreground">{item.label}</Link>
))}
</nav>
<div className="flex items-center gap-2">
<Button asChild className="hidden h-11 rounded-md bg-foreground px-6 text-background hover:bg-foreground/88 sm:inline-flex"><Link href="/catalog">{site.cta}</Link></Button>
<Button variant="outline" size="icon" className="rounded-md md:hidden" aria-label="Открыть меню"><MenuIcon className="size-4" /></Button>
</div>
</div>
</header>
);
}
export function SiteFooter() {
return (
<footer className="border-t border-border bg-foreground text-background">
<div className="mx-auto grid w-full max-w-[1320px] gap-10 px-5 py-12 sm:px-8 md:grid-cols-[1.4fr_1fr_1fr]">
<div><div className="text-2xl font-semibold">{site.name}</div><p className="mt-4 max-w-[420px] text-sm leading-6 text-background/64">{site.tagline}</p></div>
<div className="grid gap-2 text-sm text-background/70"><div className="mb-2 font-semibold text-background">Коллекция</div>{site.nav.slice(0, 4).map((item) => <Link key={item.href} href={item.href}>{item.label}</Link>)}</div>
<div className="text-sm leading-6 text-background/70"><div className="mb-2 font-semibold text-background">Шаблон</div>Product-first ecommerce с каталогом, PDP, lookbook, корзиной и сервисной страницей.</div>
</div>
</footer>
);
}
export function PageHero(_props: Record<string, unknown> = {}) {
return (
<section className="relative overflow-hidden border-b border-border">
<div className="fashion-fabric absolute inset-0 opacity-70" />
<div className="relative mx-auto grid min-h-[720px] w-full max-w-[1320px] items-center gap-12 px-5 py-16 sm:px-8 lg:grid-cols-[0.78fr_1.22fr]">
<div className="max-w-[620px]">
<Badge variant="outline" className="mb-8 rounded-md border-foreground/20 bg-background/70">Core collection 2026</Badge>
<h1 className="text-5xl font-semibold leading-[0.94] tracking-normal sm:text-7xl lg:text-[6.6rem]">Гардероб, который держит форму дня</h1>
<p className="mt-8 max-w-[560px] text-lg leading-8 text-muted-foreground">{site.tagline}</p>
<div className="mt-9 flex flex-col gap-3 sm:flex-row">
<Button asChild className="h-12 rounded-md bg-foreground px-6 text-background hover:bg-foreground/90"><Link href="/catalog">Смотреть drop <ArrowRightIcon className="size-4" /></Link></Button>
<Button asChild variant="outline" className="h-12 rounded-md px-6"><Link href="/lookbook">Открыть lookbook</Link></Button>
</div>
</div>
<div className="grid gap-4 lg:grid-cols-[1fr_0.72fr]">
<div className="relative min-h-[610px] overflow-hidden rounded-md bg-muted">
<Image src={site.heroImage} alt="Модель в черной коллекции Monochrome Supply" fill priority className="object-cover grayscale" sizes="(min-width: 1024px) 620px, 100vw" />
<div className="absolute bottom-4 left-4 right-4 flex items-center justify-between rounded-md bg-background/92 p-4 shadow-xl backdrop-blur">
<div><div className="text-sm font-semibold">Классическая overshirt</div><div className="text-xs text-muted-foreground">cotton twill / black / прямой крой</div></div>
<div className="text-sm font-semibold">17 900 </div>
</div>
</div>
<div className="grid gap-4">
{products.slice(1, 4).map((item) => (
<Link href="/product/classic-overshirt" key={item.name} className="group grid grid-cols-[112px_1fr] overflow-hidden rounded-md border border-border bg-card transition hover:-translate-y-0.5 hover:shadow-[0_18px_45px_rgba(0,0,0,0.08)]">
<div className="relative min-h-[154px] bg-muted"><Image src={item.image} alt={item.name} fill className="object-cover grayscale transition group-hover:grayscale-0" sizes="160px" /></div>
<div className="flex flex-col justify-between p-4"><Badge variant="secondary" className="w-fit rounded-md">{item.tag}</Badge><div><div className="font-semibold leading-tight">{item.name}</div><div className="mt-2 text-sm text-muted-foreground">{item.price}</div></div></div>
</Link>
))}
</div>
</div>
</div>
</section>
);
}
export function InnerHero({ eyebrow, title, text }: { eyebrow: string; title: string; text: string }) {
return <section className="border-b border-border bg-card"><div className="mx-auto w-full max-w-[1320px] px-5 py-14 sm:px-8"><Badge variant="outline" className="mb-6 rounded-md border-foreground/20">{eyebrow}</Badge><h1 className="max-w-[980px] text-5xl font-semibold leading-[0.98] tracking-normal sm:text-7xl">{title}</h1><p className="mt-6 max-w-[720px] text-base leading-7 text-muted-foreground sm:text-lg">{text}</p></div></section>;
}
export function CatalogToolbar() {
return <section className="mx-auto flex w-full max-w-[1320px] flex-col gap-3 px-5 py-6 sm:px-8 md:flex-row"><div className="flex min-h-12 flex-1 items-center gap-3 rounded-md border border-border bg-card px-4"><SearchIcon className="size-4 text-muted-foreground" /><Input className="border-0 bg-transparent shadow-none focus-visible:ring-0" placeholder="Поиск по модели, ткани или размеру" /></div><Button variant="outline" className="h-12 rounded-md"><FilterIcon className="size-4" /> Фильтр</Button><Button variant="outline" className="h-12 rounded-md"><SlidersHorizontalIcon className="size-4" /> Сортировка</Button></section>;
}
export function FeaturedGrid({ eyebrow, title, text, items }: { eyebrow: string; title: string; text: string; items: readonly TextItem[] }) {
return <section className="mx-auto w-full max-w-[1320px] px-5 py-14 sm:px-8"><div className="mb-8 grid gap-5 md:grid-cols-[0.8fr_1fr]"><div><div className="mb-2 text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">{eyebrow}</div><h2 className="text-4xl font-semibold leading-tight tracking-normal sm:text-6xl">{title}</h2></div><p className="max-w-[560px] text-base leading-7 text-muted-foreground">{text}</p></div><div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">{items.map((item) => <Link key={item.name} href="/product/classic-overshirt" className="group overflow-hidden rounded-md border border-border bg-card transition hover:-translate-y-1 hover:shadow-[0_24px_60px_rgba(0,0,0,0.09)]"><div className="relative aspect-[4/5] bg-muted"><Image src={item.image ?? site.heroImage} alt={item.name} fill className="object-cover grayscale transition duration-500 group-hover:scale-[1.03] group-hover:grayscale-0" sizes="(min-width: 1024px) 25vw, 50vw" /></div><div className="p-4"><div className="mb-4 flex items-center justify-between gap-3"><Badge variant="secondary" className="rounded-md">{item.tag}</Badge><span className="text-sm font-semibold">{item.price}</span></div><h3 className="text-lg font-semibold leading-tight">{item.name}</h3><p className="mt-2 text-sm leading-6 text-muted-foreground">{item.text}</p></div></Link>)}</div></section>;
}
export function MetricStrip(_props: { items?: unknown } = {}) {
return <section className="mx-auto grid w-full max-w-[1320px] gap-4 px-5 py-8 sm:px-8 md:grid-cols-3">{highlights.map((item) => { const Icon = item.icon; return <article key={item.title} className="rounded-md border border-border bg-card p-5"><Icon className="mb-8 size-5" /><div className="text-4xl font-semibold tracking-normal">{item.value}</div><h3 className="mt-3 font-semibold">{item.title}</h3><p className="mt-2 text-sm leading-6 text-muted-foreground">{item.text}</p></article>; })}</section>;
}
export function CollectionBoard() {
const looks = [{ title: "01 / Layered city", image: site.heroImage, text: "overshirt поверх плотного jersey" }, { title: "02 / Evening black", image: site.accentImage, text: "широкие брюки и структурный верх" }, { title: "03 / Travel shell", image: productImages[2], text: "сумка utility и легкий слой" }] as const;
return <section className="mx-auto w-full max-w-[1320px] px-5 py-16 sm:px-8"><div className="mb-9 flex flex-col gap-4 md:flex-row md:items-end md:justify-between"><h2 className="max-w-[760px] text-4xl font-semibold leading-tight sm:text-6xl">Lookbook говорит о посадке, ткани и комплекте</h2><Button asChild variant="outline" className="h-11 w-fit rounded-md"><Link href="/catalog">В каталог <ArrowRightIcon className="size-4" /></Link></Button></div><div className="grid gap-4 md:grid-cols-3">{looks.map((look, index) => <article key={look.title} className={index === 1 ? "overflow-hidden rounded-md bg-foreground text-background" : "overflow-hidden rounded-md border border-border bg-card"}><div className="relative min-h-[420px] bg-muted"><Image src={look.image} alt={look.title} fill className="object-cover grayscale" sizes="(min-width: 768px) 33vw, 100vw" /></div><div className="p-5"><h3 className="text-lg font-semibold">{look.title}</h3><p className={index === 1 ? "mt-2 text-sm text-background/70" : "mt-2 text-sm text-muted-foreground"}>{look.text}</p></div></article>)}</div></section>;
}
export function ProductDetail() {
return <section className="mx-auto grid w-full max-w-[1320px] gap-12 px-5 py-14 sm:px-8 lg:grid-cols-[1.04fr_0.96fr]"><div className="grid gap-4"><div className="relative min-h-[680px] overflow-hidden rounded-md bg-muted"><Image src={site.heroImage} alt="Классическая overshirt" fill priority className="object-cover grayscale" sizes="(min-width: 1024px) 620px, 100vw" /></div><div className="grid grid-cols-4 gap-3">{[site.heroImage, site.accentImage, productImages[1], productImages[2]].map((image) => <div key={image} className="relative aspect-square overflow-hidden rounded-md bg-muted"><Image src={image} alt="Деталь товара" fill className="object-cover grayscale" sizes="160px" /></div>)}</div></div><div className="py-4"><div className="mb-5 flex items-center gap-2 text-sm text-muted-foreground">Каталог <ChevronRightIcon className="size-4" /> Верхний слой <ChevronRightIcon className="size-4" /> Overshirt</div><h1 className="text-5xl font-semibold leading-[0.96] sm:text-7xl">Классическая overshirt</h1><div className="mt-6 flex items-center gap-3"><Badge variant="outline" className="rounded-md">4.8 </Badge><span className="text-sm text-muted-foreground">210 отзывов</span></div><div className="mt-9 flex flex-wrap items-end gap-4"><div className="text-5xl font-semibold">17 900 </div><div className="pb-1 text-sm text-muted-foreground line-through">22 900 </div><Badge className="rounded-md bg-secondary text-secondary-foreground hover:bg-secondary">Скидка 20%</Badge></div><p className="mt-7 text-lg leading-8 text-muted-foreground">Плотный cotton twill, прямой крой, скрытая планка и аккуратный вес для межсезонья.</p><Separator className="my-8" /><OptionRow label="Цвет" options={["черный", "графит", "молочный", "шалфей"]} /><OptionRow label="Размер" options={["XS", "S", "M", "L", "XL"]} active="L" /><div className="mt-8 grid gap-3 sm:grid-cols-2"><Button className="h-12 rounded-md bg-foreground text-background hover:bg-foreground/90"><ShoppingCartIcon className="size-4" /> В корзину</Button><Button variant="outline" className="h-12 rounded-md"><HeartIcon className="size-4" /> В избранное</Button></div><ProductAssurance compact /></div></section>;
}
function OptionRow({ label, options, active }: { label: string; options: readonly string[]; active?: string }) {
return <div className="mb-6"><div className="mb-3 text-sm font-semibold">{label}</div><div className="flex flex-wrap gap-2">{options.map((option) => <Button key={option} variant={option === active ? "default" : "outline"} className="h-10 rounded-md px-4">{option}</Button>)}</div></div>;
}
export function ProductAssurance({ compact = false }: { compact?: boolean }) {
const rows = [["Материал", "Плотный хлопковый twill держит форму и не выглядит спортивно."], ["Посадка", "Прямой крой оставляет место под слой и не утяжеляет силуэт."], ["После покупки", "30 дней на возврат, примерочный интервал и подсказки по уходу."]] as const;
return <section className={compact ? "mt-8" : "mx-auto w-full max-w-[1320px] px-5 py-12 sm:px-8"}><div className="overflow-hidden rounded-md border border-border bg-card">{rows.map(([title, text]) => <div key={title} className="grid gap-2 border-b border-border p-4 last:border-b-0 sm:grid-cols-[150px_1fr]"><h3 className="font-semibold">{title}</h3><p className="text-sm leading-6 text-muted-foreground">{text}</p></div>)}</div></section>;
}
export function PricingTiles({ title, items }: { title: string; items: readonly TileItem[] }) {
return <section className="mx-auto w-full max-w-[1320px] px-5 py-14 sm:px-8"><h2 className="mb-7 text-4xl font-semibold sm:text-6xl">{title}</h2><div className="grid gap-4 md:grid-cols-3">{items.map((item) => <article key={item.title} className="rounded-md border border-border bg-card p-6"><h3 className="text-2xl font-semibold">{item.title}</h3><div className="mt-3 text-4xl font-semibold">{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-foreground" />{line}</div>)}</div></article>)}</div></section>;
}
export function LookbookGrid() {
const images = [site.heroImage, site.accentImage, productImages[0], productImages[1], productImages[2], productImages[3]];
return <section className="mx-auto grid w-full max-w-[1320px] gap-4 px-5 py-12 sm:px-8 md:grid-cols-4">{images.map((image, index) => <div key={image} className={index === 0 || index === 5 ? "relative min-h-[520px] overflow-hidden rounded-md bg-muted md:col-span-2 md:row-span-2" : "relative min-h-[250px] overflow-hidden rounded-md bg-muted"}><Image src={image} alt="Lookbook Monochrome Supply" fill className="object-cover grayscale" sizes="(min-width: 768px) 25vw, 100vw" /></div>)}</section>;
}
export function CartPreview() {
return <section className="mx-auto grid w-full max-w-[1320px] gap-8 px-5 py-12 sm:px-8 lg:grid-cols-[1fr_380px]"><div className="grid gap-4">{products.slice(0, 3).map((item, index) => <article key={item.name} className="grid grid-cols-[112px_1fr_auto] items-center gap-5 rounded-md border border-border bg-card p-4"><div className="relative aspect-square overflow-hidden rounded-md bg-muted"><Image src={item.image} alt={item.name} fill className="object-cover grayscale" sizes="120px" /></div><div><h3 className="font-semibold">{item.name}</h3><p className="mt-1 text-sm text-muted-foreground">{index === 0 ? "black / L" : "black / M"}</p></div><div className="flex items-center gap-3"><Button variant="outline" size="icon" className="rounded-md"><MinusIcon className="size-4" /></Button><span className="font-semibold">1</span><Button variant="outline" size="icon" className="rounded-md"><PlusIcon className="size-4" /></Button></div></article>)}</div><aside className="h-fit rounded-md border border-border bg-card p-6"><h2 className="text-2xl font-semibold">Итог</h2><div className="mt-6 grid gap-3 text-sm"><div className="flex justify-between"><span>Товары</span><span>42 100 </span></div><div className="flex justify-between"><span>Доставка</span><span>0 </span></div><Separator /><div className="flex justify-between text-lg font-semibold"><span>К оплате</span><span>42 100 </span></div></div><Button className="mt-6 h-12 w-full rounded-md bg-foreground text-background hover:bg-foreground/90">Оформить заказ</Button></aside></section>;
}
export function IconCards({ items = eventTypes }: { items?: readonly IconItem[] }) {
return <section className="mx-auto grid w-full max-w-[1320px] gap-4 px-5 py-12 sm:px-8 md:grid-cols-3">{items.map((item) => { const Icon = item.icon; return <article key={item.title} className="rounded-md border border-border bg-card p-6"><Icon className="mb-8 size-5" /><h3 className="text-xl font-semibold">{item.title}</h3><p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p></article>; })}</section>;
}
export function InfoColumns({ title, items }: { title: string; items: readonly { title: string; text: string }[] }) {
return <section className="mx-auto w-full max-w-[1320px] px-5 py-12 sm:px-8"><h2 className="mb-6 text-4xl font-semibold">{title}</h2><div className="grid gap-4 md:grid-cols-3">{items.map((item) => <article key={item.title} className="rounded-md border border-border bg-card p-6"><h3 className="text-xl font-semibold">{item.title}</h3><p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p></article>)}</div></section>;
}
export function SplitStory({ image, eyebrow, title, text, points }: { image: string; eyebrow: string; title: string; text: string; points: readonly string[] }) {
return <section className="mx-auto grid w-full max-w-[1320px] gap-10 px-5 py-14 sm:px-8 lg:grid-cols-2"><div className="relative min-h-[520px] overflow-hidden rounded-md bg-muted"><Image src={image} alt={title} fill className="object-cover grayscale" sizes="(min-width: 1024px) 50vw, 100vw" /></div><div className="flex flex-col justify-center"><div className="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">{eyebrow}</div><h2 className="text-4xl font-semibold leading-tight sm:text-6xl">{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" />{point}</div>)}</div></div></section>;
}
export function TestimonialBand(_props: { items?: unknown } = {}) {
return <section className="mx-auto grid w-full max-w-[1320px] gap-4 px-5 py-12 sm:px-8 md:grid-cols-2">{testimonials.map((item) => <blockquote key={item.name} className="rounded-md border border-border bg-card p-7"><div className="text-sm font-semibold">{item.rating}</div><p className="mt-5 text-2xl font-semibold leading-snug">{item.text}</p><footer className="mt-5 text-sm text-muted-foreground">{item.name}</footer></blockquote>)}</section>;
}
export function CtaPanel({ title, text, href, label }: { title: string; text: string; href: string; label: string }) {
return <section className="mx-auto w-full max-w-[1320px] px-5 py-12 sm:px-8"><div className="rounded-md bg-foreground p-8 text-background"><h2 className="text-4xl font-semibold">{title}</h2><p className="mt-4 max-w-[720px] text-sm leading-7 text-background/70">{text}</p><Button asChild variant="secondary" className="mt-6 rounded-md"><Link href={href}>{label}<ArrowRightIcon className="size-4" /></Link></Button></div></section>;
}