feat: split big file and update agents.md
This commit is contained in:
51
AGENTS.md
51
AGENTS.md
@@ -5,7 +5,56 @@ Northyard Residences — premium real-estate шаблон девелопера:
|
|||||||
## Project Specifics
|
## Project Specifics
|
||||||
|
|
||||||
- Квартиры, локация, amenities и этапы покупки описаны в `src/entities/site-content.ts`.
|
- Квартиры, локация, amenities и этапы покупки описаны в `src/entities/site-content.ts`.
|
||||||
- `src/app` — только маршруты; страницы собираются в `src/widgets/template-ui.tsx`.
|
- `src/app` — только маршруты; композиция каждой страницы живёт в отдельном widget (`src/widgets/<page>-page.tsx`). См. File Map.
|
||||||
- Mock-калькуляторы держи в `src/features/*/ui`; не подключай реальные банки, CRM, карты или формы отправки без запроса.
|
- Mock-калькуляторы держи в `src/features/*/ui`; не подключай реальные банки, CRM, карты или формы отправки без запроса.
|
||||||
- Не превращай шаблон в общий лендинг: каждая страница должна отвечать на конкретный вопрос покупателя недвижимости.
|
- Не превращай шаблон в общий лендинг: каждая страница должна отвечать на конкретный вопрос покупателя недвижимости.
|
||||||
- Проверка после правок: `pnpm lint` и `pnpm build`.
|
- Проверка после правок: `pnpm lint` и `pnpm build`.
|
||||||
|
|
||||||
|
## Design System
|
||||||
|
|
||||||
|
Источник токенов — `src/app/globals.css` (`@theme inline` + `:root`/`.dark`). Шрифт — **Noto Sans** (`--font-northyard`), один гарнитур на весь UI, с включёнными OpenType-фичами `ss01`/`cv01` на `body`. Работай через семантические классы Tailwind (`bg-primary`, `text-foreground`, `border-border`, `text-muted-foreground`), никогда не хардкодь hex/oklch.
|
||||||
|
|
||||||
|
Личность: **тёплый «каменный» premium real-estate** — палитра построена на тёплых бежево-песочных оттенках (hue ~83–90), почти без насыщенности. Фон — тёплый камень/песок, primary — глубокий тёмно-коричнево-серый «графит» (НЕ синий и НЕ чёрный), secondary и accent — приглушённый шалфейно-зелёный (hue ~120–128). Тон спокойный, дорогой, архитектурный: воздух, светлые карточки, тонкие линии.
|
||||||
|
|
||||||
|
| Роль | Light | Характер |
|
||||||
|
|---|---|---|
|
||||||
|
| `background` | тёплый светлый камень/песок | основной фон страниц |
|
||||||
|
| `foreground` | тёмный тёплый графит | текст |
|
||||||
|
| `primary` | глубокий графит (тёплый) | бренд: лого-плашка, CTA-кнопки, числа, акцент линий планировок |
|
||||||
|
| `secondary` | приглушённый светлый шалфей | бейджи, мягкие плашки (`bg-secondary text-foreground`) |
|
||||||
|
| `accent` / `ring` | шалфейно-зелёный | hover/focus-кольца, точечные акценты |
|
||||||
|
| `muted` / `muted-foreground` | светлый песок / тёплый серо-бежевый | вторичный текст, подписи, hover-фон строк |
|
||||||
|
| `card` | почти белый тёплый | карточки, таблицы, sticky-панели на каменном фоне |
|
||||||
|
| `border` | тёплый бежевый | тонкие границы (1px) повсюду |
|
||||||
|
|
||||||
|
Узнаваемые приёмы (держи их — это «лицо» шаблона):
|
||||||
|
- **Мягкие углы:** `--radius` = 0.5rem; карточки и панели `rounded-lg`, мелкие плашки `rounded-md`, лого-иконка `rounded-full`.
|
||||||
|
- **Тонкие тёплые границы:** `border` (1px) на каждой карточке/строке; структура держится на линиях, а не на тенях.
|
||||||
|
- **Bento-сетки:** крупные `grid` с асимметричными колонками (`lg:grid-cols-[1.1fr_0.9fr]`, `[1fr_380px]`, sticky правая колонка `lg:sticky lg:top-24`).
|
||||||
|
- **Каменные фоны секций:** утилита `.northyard-stone` (тёплый radial+linear градиент песок/шалфей) для hero и contacts.
|
||||||
|
- **Планировочная сетка:** утилита `.floor-grid` (32px графитовая сетка-миллиметровка) для floor plan и карты локации; зоны планировки — `border-2 border-primary/*`.
|
||||||
|
- **Типографика:** крупные `font-semibold` заголовки (`text-5xl`…`text-7xl`, плотный `leading-[0.98]`), мягкие eyebrow-бейджи `variant="outline"`.
|
||||||
|
|
||||||
|
Do / Don't:
|
||||||
|
- **Do:** держи тёплую каменную палитру и шалфейный акцент; используй bento-сетки, light-карточки, sticky-панель заявки/ипотеки; продавай ежедневный сценарий покупателя (планировка, маршруты, сервис).
|
||||||
|
- **Don't:** не вводи холодный синий/чёрный primary, насыщенные или неоновые цвета, тяжёлые тени, острые `rounded-none` углы или generic SaaS-hero с градиентом — это ломает дорогой архитектурный тон.
|
||||||
|
|
||||||
|
## File Map
|
||||||
|
|
||||||
|
| Route | Widget |
|
||||||
|
|---|---|
|
||||||
|
| `/` | `src/widgets/home-page.tsx` (`HomePage`) |
|
||||||
|
| `/apartments` | `src/widgets/apartments-page.tsx` (`ApartmentsPage`) |
|
||||||
|
| `/apartments/a-1204` | `src/widgets/apartment-detail-page.tsx` (`ApartmentDetailPage`) |
|
||||||
|
| `/location` | `src/widgets/location-page.tsx` (`LocationPage`) |
|
||||||
|
| `/amenities` | `src/widgets/amenities-page.tsx` (`AmenitiesPage`) |
|
||||||
|
| `/purchase` | `src/widgets/purchase-page.tsx` (`PurchasePage`) |
|
||||||
|
| `/contacts` | `src/widgets/contacts-page.tsx` (`ContactsPage`) |
|
||||||
|
|
||||||
|
Переиспользуемые блоки:
|
||||||
|
- `src/widgets/site-shell.tsx` — `SiteShell` (header + nav, обёртка всех страниц).
|
||||||
|
- `src/widgets/apartment-rows.tsx` — `ApartmentRows` (Home, Apartments; рендерит `apartments`).
|
||||||
|
- `src/shared/ui/section-header.tsx` — `SectionHeader` (заголовок секций: Home, Apartments, Location, Amenities, Purchase).
|
||||||
|
- `src/features/mortgage-calculator/ui/mortgage-calculator.tsx` — `MortgageCalculator` (mock-калькулятор: Home, Purchase).
|
||||||
|
|
||||||
|
Одноразовые блоки колоцированы со своей страницей: `FloorPlanCard` (план квартиры) живёт внутри `src/widgets/apartment-detail-page.tsx`.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AmenitiesPage } from "@/widgets/template-ui";
|
import { AmenitiesPage } from "@/widgets/amenities-page";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <AmenitiesPage />;
|
return <AmenitiesPage />;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ApartmentDetailPage } from "@/widgets/template-ui";
|
import { ApartmentDetailPage } from "@/widgets/apartment-detail-page";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <ApartmentDetailPage />;
|
return <ApartmentDetailPage />;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ApartmentsPage } from "@/widgets/template-ui";
|
import { ApartmentsPage } from "@/widgets/apartments-page";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <ApartmentsPage />;
|
return <ApartmentsPage />;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ContactsPage } from "@/widgets/template-ui";
|
import { ContactsPage } from "@/widgets/contacts-page";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <ContactsPage />;
|
return <ContactsPage />;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LocationPage } from "@/widgets/template-ui";
|
import { LocationPage } from "@/widgets/location-page";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <LocationPage />;
|
return <LocationPage />;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HomePage } from "@/widgets/template-ui";
|
import { HomePage } from "@/widgets/home-page";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <HomePage />;
|
return <HomePage />;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PurchasePage } from "@/widgets/template-ui";
|
import { PurchasePage } from "@/widgets/purchase-page";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <PurchasePage />;
|
return <PurchasePage />;
|
||||||
|
|||||||
13
src/shared/ui/section-header.tsx
Normal file
13
src/shared/ui/section-header.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Badge } from "@/shared/ui/badge";
|
||||||
|
|
||||||
|
export function SectionHeader({ label, title, text }: { label: string; title: string; text: string }) {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto mb-8 max-w-7xl px-4 md:px-6">
|
||||||
|
<Badge variant="outline" className="mb-4 bg-card">{label}</Badge>
|
||||||
|
<div className="grid gap-4 md:grid-cols-[0.9fr_0.8fr] md:items-end">
|
||||||
|
<h1 className="text-4xl font-semibold tracking-normal md:text-6xl">{title}</h1>
|
||||||
|
<p className="text-lg leading-8 text-muted-foreground">{text}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
src/widgets/amenities-page.tsx
Normal file
18
src/widgets/amenities-page.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { TreesIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { amenities } from "@/entities/site-content";
|
||||||
|
import { SectionHeader } from "@/shared/ui/section-header";
|
||||||
|
import { SiteShell } from "@/widgets/site-shell";
|
||||||
|
|
||||||
|
export function AmenitiesPage() {
|
||||||
|
return (
|
||||||
|
<SiteShell>
|
||||||
|
<section className="py-12 md:py-18">
|
||||||
|
<SectionHeader label="Сервис" title="Инфраструктура не для буклета, а для будней" text="Блоки отвечают на бизнес-вопрос покупателя: что будет удобно каждый день после сделки." />
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-5 px-4 md:grid-cols-2 md:px-6">
|
||||||
|
{amenities.map((item, index) => <article key={item.title} className="min-h-72 rounded-lg border bg-card p-6"><TreesIcon className="mb-12 size-8 text-primary" /><div className="text-sm text-muted-foreground">0{index + 1}</div><h2 className="mt-3 text-3xl font-semibold">{item.title}</h2><p className="mt-4 leading-7 text-muted-foreground">{item.text}</p></article>)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</SiteShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
src/widgets/apartment-detail-page.tsx
Normal file
49
src/widgets/apartment-detail-page.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { CheckIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { apartments } from "@/entities/site-content";
|
||||||
|
import { Badge } from "@/shared/ui/badge";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import { SiteShell } from "@/widgets/site-shell";
|
||||||
|
|
||||||
|
function FloorPlanCard() {
|
||||||
|
return (
|
||||||
|
<div className="floor-grid relative min-h-[420px] overflow-hidden rounded-lg border bg-card p-5">
|
||||||
|
<div className="absolute inset-6 grid grid-cols-6 grid-rows-5 gap-2">
|
||||||
|
<div className="col-span-4 row-span-3 rounded-md border-2 border-primary/70 bg-background/80 p-3 text-sm font-medium">Гостиная 32.4 м²</div>
|
||||||
|
<div className="col-span-2 row-span-2 rounded-md border-2 border-primary/50 bg-secondary/45 p-3 text-sm">Кухня</div>
|
||||||
|
<div className="col-span-2 row-span-2 rounded-md border-2 border-primary/50 bg-background/80 p-3 text-sm">Мастер</div>
|
||||||
|
<div className="col-span-2 row-span-2 rounded-md border-2 border-primary/50 bg-background/80 p-3 text-sm">Детская</div>
|
||||||
|
<div className="col-span-1 rounded-md border-2 border-primary/40 bg-muted p-2 text-xs">С/у</div>
|
||||||
|
<div className="col-span-1 rounded-md border-2 border-primary/40 bg-muted p-2 text-xs">Пост.</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-5 left-5 rounded-full bg-primary px-4 py-2 text-sm font-medium text-primary-foreground">A-1204 / 96.4 м²</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApartmentDetailPage() {
|
||||||
|
const apartment = apartments[0];
|
||||||
|
return (
|
||||||
|
<SiteShell>
|
||||||
|
<section className="px-4 py-10 md:px-6 md:py-16">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[1fr_420px]">
|
||||||
|
<div>
|
||||||
|
<Badge className="mb-5">{apartment.status}</Badge>
|
||||||
|
<h1 className="text-5xl font-semibold md:text-7xl">Квартира {apartment.id}</h1>
|
||||||
|
<p className="mt-4 max-w-2xl text-lg leading-8 text-muted-foreground">{apartment.rooms}, {apartment.area}, {apartment.plan}. Вид на {apartment.view}, этаж {apartment.floor}.</p>
|
||||||
|
<div className="mt-8"><FloorPlanCard /></div>
|
||||||
|
</div>
|
||||||
|
<aside className="rounded-lg border bg-card p-5 shadow-sm lg:sticky lg:top-24 lg:self-start">
|
||||||
|
<div className="text-sm text-muted-foreground">Стоимость</div>
|
||||||
|
<div className="mt-1 text-4xl font-semibold">{apartment.price}</div>
|
||||||
|
<div className="mt-6 grid gap-3 text-sm">
|
||||||
|
{['Отделка white box', 'Окна в пол', '2 санузла', 'Семейная кухня-гостиная'].map((item) => <div key={item} className="flex items-center gap-2"><CheckIcon className="size-4 text-primary" />{item}</div>)}
|
||||||
|
</div>
|
||||||
|
<Button asChild className="mt-6 w-full"><Link href="/contacts">Забронировать просмотр</Link></Button>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</SiteShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
src/widgets/apartment-rows.tsx
Normal file
21
src/widgets/apartment-rows.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { apartments } from "@/entities/site-content";
|
||||||
|
import { Badge } from "@/shared/ui/badge";
|
||||||
|
|
||||||
|
export function ApartmentRows() {
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden rounded-lg border bg-card">
|
||||||
|
{apartments.map((apartment) => (
|
||||||
|
<Link key={apartment.id} href={apartment.id === "A-1204" ? "/apartments/a-1204" : "/apartments"} className="grid gap-3 border-b p-4 transition last:border-b-0 hover:bg-muted/60 md:grid-cols-[120px_1fr_120px_120px_160px_120px] md:items-center">
|
||||||
|
<span className="font-semibold">{apartment.id}</span>
|
||||||
|
<span>{apartment.rooms} / {apartment.area}</span>
|
||||||
|
<span className="text-muted-foreground">этаж {apartment.floor}</span>
|
||||||
|
<span className="text-muted-foreground">{apartment.view}</span>
|
||||||
|
<span className="font-semibold">{apartment.price}</span>
|
||||||
|
<Badge variant={apartment.status === "доступна" ? "default" : "secondary"}>{apartment.status}</Badge>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/widgets/apartments-page.tsx
Normal file
23
src/widgets/apartments-page.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { HomeIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { SectionHeader } from "@/shared/ui/section-header";
|
||||||
|
import { ApartmentRows } from "@/widgets/apartment-rows";
|
||||||
|
import { SiteShell } from "@/widgets/site-shell";
|
||||||
|
|
||||||
|
export function ApartmentsPage() {
|
||||||
|
return (
|
||||||
|
<SiteShell>
|
||||||
|
<section className="py-12 md:py-18">
|
||||||
|
<SectionHeader label="Каталог" title="Квартиры по корпусам, видам и сценариям" text="Каталог показывает не только площадь и цену, но и планировочную пользу: хранение, приватность, рабочие зоны и ориентацию окон." />
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-6 px-4 md:px-6 lg:grid-cols-[1fr_380px]">
|
||||||
|
<ApartmentRows />
|
||||||
|
<div className="rounded-lg border bg-card p-5">
|
||||||
|
<HomeIcon className="mb-5 size-7" />
|
||||||
|
<h2 className="text-2xl font-semibold">Фильтры для будущего CMS</h2>
|
||||||
|
<p className="mt-3 leading-7 text-muted-foreground">Комнаты, корпус, вид, отделка, статус брони и диапазон цены уже отражены в интерфейсе как production-ready блоки.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</SiteShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
src/widgets/contacts-page.tsx
Normal file
25
src/widgets/contacts-page.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { site } from "@/entities/site-content";
|
||||||
|
import { Badge } from "@/shared/ui/badge";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import { SiteShell } from "@/widgets/site-shell";
|
||||||
|
|
||||||
|
export function ContactsPage() {
|
||||||
|
return (
|
||||||
|
<SiteShell>
|
||||||
|
<section className="northyard-stone px-4 py-12 md:px-6 md:py-18">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[1fr_420px]">
|
||||||
|
<div className="rounded-lg border bg-card/90 p-6 md:p-10">
|
||||||
|
<Badge className="mb-6">Отдел продаж</Badge>
|
||||||
|
<h1 className="text-5xl font-semibold md:text-7xl">Запишитесь на приватный просмотр</h1>
|
||||||
|
<p className="mt-6 text-lg leading-8 text-muted-foreground">Менеджер подготовит подборку квартир, планировки и финансовый сценарий до визита.</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border bg-card p-6 shadow-sm">
|
||||||
|
<div className="text-sm text-muted-foreground">Телефон</div><div className="text-2xl font-semibold">{site.phone}</div>
|
||||||
|
<div className="mt-6 text-sm text-muted-foreground">Email</div><div className="text-2xl font-semibold">{site.email}</div>
|
||||||
|
<Button className="mt-8 w-full">Назначить просмотр</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</SiteShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
src/widgets/home-page.tsx
Normal file
48
src/widgets/home-page.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { ArrowRightIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { site } from "@/entities/site-content";
|
||||||
|
import { MortgageCalculator } from "@/features/mortgage-calculator/ui/mortgage-calculator";
|
||||||
|
import { Badge } from "@/shared/ui/badge";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import { SectionHeader } from "@/shared/ui/section-header";
|
||||||
|
import { ApartmentRows } from "@/widgets/apartment-rows";
|
||||||
|
import { SiteShell } from "@/widgets/site-shell";
|
||||||
|
|
||||||
|
export function HomePage() {
|
||||||
|
return (
|
||||||
|
<SiteShell>
|
||||||
|
<section className="northyard-stone px-4 py-8 md:px-6 md:py-12">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-5 lg:grid-cols-[1.1fr_0.9fr]">
|
||||||
|
<div className="rounded-lg border bg-card/80 p-6 shadow-sm md:p-10">
|
||||||
|
<Badge className="mb-8 bg-secondary text-foreground">Ключи с IV квартала 2026</Badge>
|
||||||
|
<h1 className="max-w-4xl text-5xl font-semibold leading-[0.98] md:text-7xl">Квартал с приватным двором и городом в пешей доступности</h1>
|
||||||
|
<p className="mt-6 max-w-2xl text-lg leading-8 text-muted-foreground">{site.name} соединяет клубный сервис, спокойные планировки и инфраструктуру района без ощущения закрытого острова.</p>
|
||||||
|
<div className="mt-8 flex flex-wrap gap-3">
|
||||||
|
<Button asChild size="lg"><Link href="/apartments">Выбрать квартиру <ArrowRightIcon className="size-4" /></Link></Button>
|
||||||
|
<Button asChild variant="outline" size="lg"><Link href="/location">Смотреть локацию</Link></Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-5 md:grid-cols-2 lg:grid-cols-1">
|
||||||
|
<div className="relative min-h-[320px] overflow-hidden rounded-lg border">
|
||||||
|
<Image src="https://images.unsplash.com/photo-1600607688969-a5bfcd646154?auto=format&fit=crop&w=1400&q=82" alt="Лобби жилого квартала" fill priority className="object-cover" sizes="(min-width: 1024px) 40vw, 100vw" />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
{["214 квартир", "2 корпуса", "0 машин во дворе"].map((item) => <div key={item} className="rounded-lg border bg-card p-4 text-sm font-semibold shadow-sm">{item}</div>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="px-4 py-12 md:px-6 md:py-18">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-5 lg:grid-cols-[1fr_380px]">
|
||||||
|
<div>
|
||||||
|
<SectionHeader label="Квартиры" title="Планировки, которые держат сценарий семьи" text="От студий для инвестиций до угловых квартир с мастер-блоком, постирочной и окнами на тихий двор." />
|
||||||
|
<div className="px-4 md:px-6"><ApartmentRows /></div>
|
||||||
|
</div>
|
||||||
|
<div className="lg:sticky lg:top-24 lg:self-start"><MortgageCalculator /></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</SiteShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
src/widgets/location-page.tsx
Normal file
22
src/widgets/location-page.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { MapPinIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { locationPoints, site } from "@/entities/site-content";
|
||||||
|
import { SectionHeader } from "@/shared/ui/section-header";
|
||||||
|
import { SiteShell } from "@/widgets/site-shell";
|
||||||
|
|
||||||
|
export function LocationPage() {
|
||||||
|
return (
|
||||||
|
<SiteShell>
|
||||||
|
<section className="py-12 md:py-18">
|
||||||
|
<SectionHeader label="Локация" title="Город рядом, двор остается тихим" text="Страница продает не абстрактный адрес, а ежедневные маршруты: школа, парк, офис, сервисы и вечерние прогулки." />
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-5 px-4 md:px-6 lg:grid-cols-[1fr_1fr]">
|
||||||
|
<div className="floor-grid min-h-[520px] rounded-lg border bg-card p-6">
|
||||||
|
<MapPinIcon className="mb-20 size-10 text-primary" />
|
||||||
|
<div className="rounded-lg bg-primary p-5 text-primary-foreground shadow-lg md:w-72">{site.address}<br />главный вход с пешеходной улицы</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4">{locationPoints.map((point) => <article key={point.title} className="rounded-lg border bg-card p-5"><div className="text-3xl font-semibold">{point.time}</div><h2 className="mt-3 text-xl font-semibold">{point.title}</h2><p className="mt-2 text-muted-foreground">{point.text}</p></article>)}</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</SiteShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
src/widgets/purchase-page.tsx
Normal file
25
src/widgets/purchase-page.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { construction, purchaseSteps } from "@/entities/site-content";
|
||||||
|
import { MortgageCalculator } from "@/features/mortgage-calculator/ui/mortgage-calculator";
|
||||||
|
import { Progress } from "@/shared/ui/progress";
|
||||||
|
import { SectionHeader } from "@/shared/ui/section-header";
|
||||||
|
import { SiteShell } from "@/widgets/site-shell";
|
||||||
|
|
||||||
|
export function PurchasePage() {
|
||||||
|
return (
|
||||||
|
<SiteShell>
|
||||||
|
<section className="py-12 md:py-18">
|
||||||
|
<SectionHeader label="Покупка" title="Понятный путь от выбора до ключей" text="Шаблон закрывает блоки, которые нужны девелоперу: бронь, ипотека, документы, ход строительства и контакты отдела продаж." />
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-6 px-4 md:px-6 lg:grid-cols-[0.9fr_1.1fr]">
|
||||||
|
<MortgageCalculator />
|
||||||
|
<div className="space-y-4">
|
||||||
|
{purchaseSteps.map((step, index) => <div key={step} className="rounded-lg border bg-card p-5"><span className="text-sm text-muted-foreground">0{index + 1}</span><h2 className="mt-2 text-2xl font-semibold">{step}</h2></div>)}
|
||||||
|
<div className="rounded-lg border bg-card p-5">
|
||||||
|
<h2 className="mb-4 text-2xl font-semibold">Ход строительства</h2>
|
||||||
|
<div className="grid gap-4">{construction.map((item) => <div key={item.label}><div className="mb-2 flex justify-between text-sm"><span>{item.label}</span><span>{item.value}%</span></div><Progress value={item.value} /></div>)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</SiteShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
src/widgets/site-shell.tsx
Normal file
26
src/widgets/site-shell.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Building2Icon } from "lucide-react";
|
||||||
|
|
||||||
|
import { navItems, site } from "@/entities/site-content";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
|
||||||
|
export function SiteShell({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen bg-background text-foreground">
|
||||||
|
<header className="sticky top-0 z-40 border-b bg-background/90 backdrop-blur">
|
||||||
|
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 md:px-6">
|
||||||
|
<Link href="/" className="flex items-center gap-2 font-semibold">
|
||||||
|
<span className="rounded-full bg-primary p-2 text-primary-foreground"><Building2Icon className="size-4" /></span>
|
||||||
|
{site.name}
|
||||||
|
</Link>
|
||||||
|
<nav className="hidden items-center gap-6 text-sm text-muted-foreground md:flex">
|
||||||
|
{navItems.map((item) => <Link key={item.href} href={item.href} className="hover:text-foreground">{item.label}</Link>)}
|
||||||
|
</nav>
|
||||||
|
<Button asChild size="sm"><Link href="/contacts">Записаться</Link></Button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { ArrowRightIcon, Building2Icon, CheckIcon, HomeIcon, MapPinIcon, TreesIcon } from "lucide-react";
|
|
||||||
|
|
||||||
import { MortgageCalculator } from "@/features/mortgage-calculator/ui/mortgage-calculator";
|
|
||||||
import { apartments, amenities, construction, locationPoints, navItems, purchaseSteps, site } from "@/entities/site-content";
|
|
||||||
import { Badge } from "@/shared/ui/badge";
|
|
||||||
import { Button } from "@/shared/ui/button";
|
|
||||||
import { Progress } from "@/shared/ui/progress";
|
|
||||||
|
|
||||||
function Shell({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<main className="min-h-screen bg-background text-foreground">
|
|
||||||
<header className="sticky top-0 z-40 border-b bg-background/90 backdrop-blur">
|
|
||||||
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 md:px-6">
|
|
||||||
<Link href="/" className="flex items-center gap-2 font-semibold">
|
|
||||||
<span className="rounded-full bg-primary p-2 text-primary-foreground"><Building2Icon className="size-4" /></span>
|
|
||||||
{site.name}
|
|
||||||
</Link>
|
|
||||||
<nav className="hidden items-center gap-6 text-sm text-muted-foreground md:flex">
|
|
||||||
{navItems.map((item) => <Link key={item.href} href={item.href} className="hover:text-foreground">{item.label}</Link>)}
|
|
||||||
</nav>
|
|
||||||
<Button asChild size="sm"><Link href="/contacts">Записаться</Link></Button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SectionHeader({ label, title, text }: { label: string; title: string; text: string }) {
|
|
||||||
return (
|
|
||||||
<div className="mx-auto mb-8 max-w-7xl px-4 md:px-6">
|
|
||||||
<Badge variant="outline" className="mb-4 bg-card">{label}</Badge>
|
|
||||||
<div className="grid gap-4 md:grid-cols-[0.9fr_0.8fr] md:items-end">
|
|
||||||
<h1 className="text-4xl font-semibold tracking-normal md:text-6xl">{title}</h1>
|
|
||||||
<p className="text-lg leading-8 text-muted-foreground">{text}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FloorPlanCard() {
|
|
||||||
return (
|
|
||||||
<div className="floor-grid relative min-h-[420px] overflow-hidden rounded-lg border bg-card p-5">
|
|
||||||
<div className="absolute inset-6 grid grid-cols-6 grid-rows-5 gap-2">
|
|
||||||
<div className="col-span-4 row-span-3 rounded-md border-2 border-primary/70 bg-background/80 p-3 text-sm font-medium">Гостиная 32.4 м²</div>
|
|
||||||
<div className="col-span-2 row-span-2 rounded-md border-2 border-primary/50 bg-secondary/45 p-3 text-sm">Кухня</div>
|
|
||||||
<div className="col-span-2 row-span-2 rounded-md border-2 border-primary/50 bg-background/80 p-3 text-sm">Мастер</div>
|
|
||||||
<div className="col-span-2 row-span-2 rounded-md border-2 border-primary/50 bg-background/80 p-3 text-sm">Детская</div>
|
|
||||||
<div className="col-span-1 rounded-md border-2 border-primary/40 bg-muted p-2 text-xs">С/у</div>
|
|
||||||
<div className="col-span-1 rounded-md border-2 border-primary/40 bg-muted p-2 text-xs">Пост.</div>
|
|
||||||
</div>
|
|
||||||
<div className="absolute bottom-5 left-5 rounded-full bg-primary px-4 py-2 text-sm font-medium text-primary-foreground">A-1204 / 96.4 м²</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ApartmentRows() {
|
|
||||||
return (
|
|
||||||
<div className="overflow-hidden rounded-lg border bg-card">
|
|
||||||
{apartments.map((apartment) => (
|
|
||||||
<Link key={apartment.id} href={apartment.id === "A-1204" ? "/apartments/a-1204" : "/apartments"} className="grid gap-3 border-b p-4 transition last:border-b-0 hover:bg-muted/60 md:grid-cols-[120px_1fr_120px_120px_160px_120px] md:items-center">
|
|
||||||
<span className="font-semibold">{apartment.id}</span>
|
|
||||||
<span>{apartment.rooms} / {apartment.area}</span>
|
|
||||||
<span className="text-muted-foreground">этаж {apartment.floor}</span>
|
|
||||||
<span className="text-muted-foreground">{apartment.view}</span>
|
|
||||||
<span className="font-semibold">{apartment.price}</span>
|
|
||||||
<Badge variant={apartment.status === "доступна" ? "default" : "secondary"}>{apartment.status}</Badge>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HomePage() {
|
|
||||||
return (
|
|
||||||
<Shell>
|
|
||||||
<section className="northyard-stone px-4 py-8 md:px-6 md:py-12">
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-5 lg:grid-cols-[1.1fr_0.9fr]">
|
|
||||||
<div className="rounded-lg border bg-card/80 p-6 shadow-sm md:p-10">
|
|
||||||
<Badge className="mb-8 bg-secondary text-foreground">Ключи с IV квартала 2026</Badge>
|
|
||||||
<h1 className="max-w-4xl text-5xl font-semibold leading-[0.98] md:text-7xl">Квартал с приватным двором и городом в пешей доступности</h1>
|
|
||||||
<p className="mt-6 max-w-2xl text-lg leading-8 text-muted-foreground">{site.name} соединяет клубный сервис, спокойные планировки и инфраструктуру района без ощущения закрытого острова.</p>
|
|
||||||
<div className="mt-8 flex flex-wrap gap-3">
|
|
||||||
<Button asChild size="lg"><Link href="/apartments">Выбрать квартиру <ArrowRightIcon className="size-4" /></Link></Button>
|
|
||||||
<Button asChild variant="outline" size="lg"><Link href="/location">Смотреть локацию</Link></Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-5 md:grid-cols-2 lg:grid-cols-1">
|
|
||||||
<div className="relative min-h-[320px] overflow-hidden rounded-lg border">
|
|
||||||
<Image src="https://images.unsplash.com/photo-1600607688969-a5bfcd646154?auto=format&fit=crop&w=1400&q=82" alt="Лобби жилого квартала" fill priority className="object-cover" sizes="(min-width: 1024px) 40vw, 100vw" />
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-3 gap-3">
|
|
||||||
{["214 квартир", "2 корпуса", "0 машин во дворе"].map((item) => <div key={item} className="rounded-lg border bg-card p-4 text-sm font-semibold shadow-sm">{item}</div>)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section className="px-4 py-12 md:px-6 md:py-18">
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-5 lg:grid-cols-[1fr_380px]">
|
|
||||||
<div>
|
|
||||||
<SectionHeader label="Квартиры" title="Планировки, которые держат сценарий семьи" text="От студий для инвестиций до угловых квартир с мастер-блоком, постирочной и окнами на тихий двор." />
|
|
||||||
<div className="px-4 md:px-6"><ApartmentRows /></div>
|
|
||||||
</div>
|
|
||||||
<div className="lg:sticky lg:top-24 lg:self-start"><MortgageCalculator /></div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Shell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ApartmentsPage() {
|
|
||||||
return (
|
|
||||||
<Shell>
|
|
||||||
<section className="py-12 md:py-18">
|
|
||||||
<SectionHeader label="Каталог" title="Квартиры по корпусам, видам и сценариям" text="Каталог показывает не только площадь и цену, но и планировочную пользу: хранение, приватность, рабочие зоны и ориентацию окон." />
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-6 px-4 md:px-6 lg:grid-cols-[1fr_380px]">
|
|
||||||
<ApartmentRows />
|
|
||||||
<div className="rounded-lg border bg-card p-5">
|
|
||||||
<HomeIcon className="mb-5 size-7" />
|
|
||||||
<h2 className="text-2xl font-semibold">Фильтры для будущего CMS</h2>
|
|
||||||
<p className="mt-3 leading-7 text-muted-foreground">Комнаты, корпус, вид, отделка, статус брони и диапазон цены уже отражены в интерфейсе как production-ready блоки.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Shell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ApartmentDetailPage() {
|
|
||||||
const apartment = apartments[0];
|
|
||||||
return (
|
|
||||||
<Shell>
|
|
||||||
<section className="px-4 py-10 md:px-6 md:py-16">
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[1fr_420px]">
|
|
||||||
<div>
|
|
||||||
<Badge className="mb-5">{apartment.status}</Badge>
|
|
||||||
<h1 className="text-5xl font-semibold md:text-7xl">Квартира {apartment.id}</h1>
|
|
||||||
<p className="mt-4 max-w-2xl text-lg leading-8 text-muted-foreground">{apartment.rooms}, {apartment.area}, {apartment.plan}. Вид на {apartment.view}, этаж {apartment.floor}.</p>
|
|
||||||
<div className="mt-8"><FloorPlanCard /></div>
|
|
||||||
</div>
|
|
||||||
<aside className="rounded-lg border bg-card p-5 shadow-sm lg:sticky lg:top-24 lg:self-start">
|
|
||||||
<div className="text-sm text-muted-foreground">Стоимость</div>
|
|
||||||
<div className="mt-1 text-4xl font-semibold">{apartment.price}</div>
|
|
||||||
<div className="mt-6 grid gap-3 text-sm">
|
|
||||||
{['Отделка white box', 'Окна в пол', '2 санузла', 'Семейная кухня-гостиная'].map((item) => <div key={item} className="flex items-center gap-2"><CheckIcon className="size-4 text-primary" />{item}</div>)}
|
|
||||||
</div>
|
|
||||||
<Button asChild className="mt-6 w-full"><Link href="/contacts">Забронировать просмотр</Link></Button>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Shell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LocationPage() {
|
|
||||||
return (
|
|
||||||
<Shell>
|
|
||||||
<section className="py-12 md:py-18">
|
|
||||||
<SectionHeader label="Локация" title="Город рядом, двор остается тихим" text="Страница продает не абстрактный адрес, а ежедневные маршруты: школа, парк, офис, сервисы и вечерние прогулки." />
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-5 px-4 md:px-6 lg:grid-cols-[1fr_1fr]">
|
|
||||||
<div className="floor-grid min-h-[520px] rounded-lg border bg-card p-6">
|
|
||||||
<MapPinIcon className="mb-20 size-10 text-primary" />
|
|
||||||
<div className="rounded-lg bg-primary p-5 text-primary-foreground shadow-lg md:w-72">{site.address}<br />главный вход с пешеходной улицы</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-4">{locationPoints.map((point) => <article key={point.title} className="rounded-lg border bg-card p-5"><div className="text-3xl font-semibold">{point.time}</div><h2 className="mt-3 text-xl font-semibold">{point.title}</h2><p className="mt-2 text-muted-foreground">{point.text}</p></article>)}</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Shell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AmenitiesPage() {
|
|
||||||
return (
|
|
||||||
<Shell>
|
|
||||||
<section className="py-12 md:py-18">
|
|
||||||
<SectionHeader label="Сервис" title="Инфраструктура не для буклета, а для будней" text="Блоки отвечают на бизнес-вопрос покупателя: что будет удобно каждый день после сделки." />
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-5 px-4 md:grid-cols-2 md:px-6">
|
|
||||||
{amenities.map((item, index) => <article key={item.title} className="min-h-72 rounded-lg border bg-card p-6"><TreesIcon className="mb-12 size-8 text-primary" /><div className="text-sm text-muted-foreground">0{index + 1}</div><h2 className="mt-3 text-3xl font-semibold">{item.title}</h2><p className="mt-4 leading-7 text-muted-foreground">{item.text}</p></article>)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Shell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PurchasePage() {
|
|
||||||
return (
|
|
||||||
<Shell>
|
|
||||||
<section className="py-12 md:py-18">
|
|
||||||
<SectionHeader label="Покупка" title="Понятный путь от выбора до ключей" text="Шаблон закрывает блоки, которые нужны девелоперу: бронь, ипотека, документы, ход строительства и контакты отдела продаж." />
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-6 px-4 md:px-6 lg:grid-cols-[0.9fr_1.1fr]">
|
|
||||||
<MortgageCalculator />
|
|
||||||
<div className="space-y-4">
|
|
||||||
{purchaseSteps.map((step, index) => <div key={step} className="rounded-lg border bg-card p-5"><span className="text-sm text-muted-foreground">0{index + 1}</span><h2 className="mt-2 text-2xl font-semibold">{step}</h2></div>)}
|
|
||||||
<div className="rounded-lg border bg-card p-5">
|
|
||||||
<h2 className="mb-4 text-2xl font-semibold">Ход строительства</h2>
|
|
||||||
<div className="grid gap-4">{construction.map((item) => <div key={item.label}><div className="mb-2 flex justify-between text-sm"><span>{item.label}</span><span>{item.value}%</span></div><Progress value={item.value} /></div>)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Shell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ContactsPage() {
|
|
||||||
return (
|
|
||||||
<Shell>
|
|
||||||
<section className="northyard-stone px-4 py-12 md:px-6 md:py-18">
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[1fr_420px]">
|
|
||||||
<div className="rounded-lg border bg-card/90 p-6 md:p-10">
|
|
||||||
<Badge className="mb-6">Отдел продаж</Badge>
|
|
||||||
<h1 className="text-5xl font-semibold md:text-7xl">Запишитесь на приватный просмотр</h1>
|
|
||||||
<p className="mt-6 text-lg leading-8 text-muted-foreground">Менеджер подготовит подборку квартир, планировки и финансовый сценарий до визита.</p>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-lg border bg-card p-6 shadow-sm">
|
|
||||||
<div className="text-sm text-muted-foreground">Телефон</div><div className="text-2xl font-semibold">{site.phone}</div>
|
|
||||||
<div className="mt-6 text-sm text-muted-foreground">Email</div><div className="text-2xl font-semibold">{site.email}</div>
|
|
||||||
<Button className="mt-8 w-full">Назначить просмотр</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Shell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user