feat: split big file and update agents.md

This commit is contained in:
2026-06-18 23:20:17 +03:00
parent cc014068ba
commit f0ec78b051
18 changed files with 608 additions and 539 deletions

View File

@@ -5,8 +5,57 @@ Volthouse Energy — technical blueprint шаблон инженерной эн
## Project Specifics
- Решения, проекты, сервис, financing, процесс и документы отчета описаны в `src/entities/site-content.ts`.
- `src/app` — только route wrappers; композиция страниц находится в `src/widgets/template-ui.tsx`.
- `src/app` — только route wrappers; композиция каждой страницы живёт в отдельном widget (`src/widgets/<page>-page.tsx`). Header/footer-обёртка — `src/widgets/site-shell.tsx` (`SiteShell`). См. File Map.
- Mock-калькуляторы держи в `src/features/*/ui`; не подключай реальные тарифы, счетчики, CRM или API без запроса.
- Не превращай шаблон в generic clean-tech лендинг: каждая страница должна говорить языком объекта, пиков, теплопотерь, критичных линий, CAPEX/OPEX и сервисной ответственности.
- Не добавляй декоративные абстрактные волны/градиентные пятна; визуальная система должна выглядеть как инженерная диспетчерская и проектная документация.
- Проверка после правок: `pnpm lint` и `pnpm build`.
## Design System
Источник токенов — `src/app/globals.css` (`@theme inline` + `:root`/`.dark`). Шрифт — **Roboto Flex** (`--font-volthouse`), один гарнитур на sans и mono, с `font-feature-settings: "ss01" "cv01" "tnum"` (табличные цифры — это инженерный код данных). Работай через семантические классы Tailwind (`bg-primary`, `text-muted-foreground`, `border-primary/25`), не хардкодь hex/oklch.
Личность: **dark engineering blueprint / диспетчерская** — глубокий сине-стальной фон (тёмный, low-chroma blue), янтарный `primary` (oklch 0.82 0.16 78) как «питание под напряжением», бирюзовый `accent` (oklch 0.76 0.118 177) для метрик экономии/результата, тёплый кремовый текст. Это шаблон по умолчанию ТЁМНЫЙ (`:root` уже тёмная) — светлой темы как основного режима нет.
| Роль | Значение | Характер |
|---|---|---|
| `background` | тёмный сине-стальной (0.13) | фон-диспетчерская + grid-overlay в body |
| `foreground` | тёплый кремовый (0.95) | основной текст |
| `primary` | янтарный | «под напряжением»: бейджи, иконки-плашки, метрики, CTA, `border-primary/*` |
| `accent` | бирюзовый | результат/экономия: deliverable-блоки, OPEX, итоги (`bg-accent/10 text-accent`) |
| `secondary` | приглушённый стальной | вторичные плашки |
| `muted` | тёмно-серо-синий / fg 0.73 | подписи, описания |
| `card` | чуть светлее фона (0.18) | панели и карточки |
| `border` | стальной (0.36) | границы, чаще как `border-primary/25` |
Узнаваемые приёмы (держи их, это и есть «лицо» проекта):
- **Острые углы:** `--radius` = 0.25rem; панели и карточки фактически прямоугольные — это техническая документация, а не мягкий SaaS.
- **Полупрозрачные янтарные границы:** `border border-primary/25``/20`, `/35`, `/40`) повсюду — структура диспетчерской.
- **Grid-фактуры:** `.volt-grid` — миллиметровка под hero/section-header, `.volt-panel` — двухслойная сетка + угловой glow для data-панелей; сам `body` тоже несёт grid-overlay 72px.
- **Glow вместо тени:** `shadow-[0_0_60px_oklch(0.82_0.16_78_/_0.1)]` — мягкое янтарное свечение панели, не drop-shadow.
- **Типографика:** заголовки — `font-semibold uppercase leading-none`, очень крупные (`text-6xl`/`text-7xl`); цифры-метрики крупным `text-primary`/`text-accent`.
- **Цветовое кодирование:** янтарь = вход/мощность/действие, бирюза = выход/экономия/результат. Не смешивай роли.
- **Фото:** всегда `object-cover grayscale` + градиент `from-background` снизу — снимок встроен в тёмную среду.
Do / Don't:
- **Do:** держи тёмную grid-среду, острые data-панели, янтарь/бирюзу по ролям, табличные цифры, доказательную подачу (мощность, CAPEX/OPEX, SLA).
- **Don't:** светлый фон, скруглённые карточки, drop-shadow вместо glow, пастель/градиентные пятна, generic clean-tech hero — это ломает инженерную личность.
## File Map
| Route | Widget |
|---|---|
| `/` | `src/widgets/home-page.tsx` (`HomePage`) |
| `/solutions` | `src/widgets/solutions-page.tsx` (`SolutionsPage`) |
| `/calculator` | `src/widgets/calculator-page.tsx` (`CalculatorPage`) |
| `/projects` | `src/widgets/projects-page.tsx` (`ProjectsPage`) |
| `/maintenance` | `src/widgets/maintenance-page.tsx` (`MaintenancePage`) |
| `/financing` | `src/widgets/financing-page.tsx` (`FinancingPage`) |
| `/contacts` | `src/widgets/contacts-page.tsx` (`ContactsPage`) |
Переиспользуемые блоки:
- `src/widgets/site-shell.tsx``SiteShell` (header + nav + footer, обёртка всех страниц).
- `src/shared/ui/section-header.tsx``SectionHeader` (заголовочная секция внутренних страниц: Solutions, Calculator, Projects, Maintenance, Financing, Contacts).
- `src/features/energy-calculator/ui/energy-calculator.tsx``EnergyCalculator` (mock-калькулятор, Calculator).
Одноразовые блоки колоцированы со своей страницей: `MetricStrip`/`EnergyBoard`/`SystemNode`/`StackCard`/`ProcessRail` в `home-page.tsx`, `ContactStat` в `contacts-page.tsx`.

View File

@@ -1,4 +1,4 @@
import { CalculatorPage } from "@/widgets/template-ui";
import { CalculatorPage } from "@/widgets/calculator-page";
export default function Page() {
return <CalculatorPage />;

View File

@@ -1,4 +1,4 @@
import { ContactsPage } from "@/widgets/template-ui";
import { ContactsPage } from "@/widgets/contacts-page";
export default function Page() {
return <ContactsPage />;

View File

@@ -1,4 +1,4 @@
import { FinancingPage } from "@/widgets/template-ui";
import { FinancingPage } from "@/widgets/financing-page";
export default function Page() {
return <FinancingPage />;

View File

@@ -1,4 +1,4 @@
import { MaintenancePage } from "@/widgets/template-ui";
import { MaintenancePage } from "@/widgets/maintenance-page";
export default function Page() {
return <MaintenancePage />;

View File

@@ -1,4 +1,4 @@
import { HomePage } from "@/widgets/template-ui";
import { HomePage } from "@/widgets/home-page";
export default function Page() {
return <HomePage />;

View File

@@ -1,4 +1,4 @@
import { ProjectsPage } from "@/widgets/template-ui";
import { ProjectsPage } from "@/widgets/projects-page";
export default function Page() {
return <ProjectsPage />;

View File

@@ -1,4 +1,4 @@
import { SolutionsPage } from "@/widgets/template-ui";
import { SolutionsPage } from "@/widgets/solutions-page";
export default function Page() {
return <SolutionsPage />;

View File

@@ -0,0 +1,15 @@
import { Badge } from "@/shared/ui/badge";
export function SectionHeader({ label, title, text }: { label: string; title: string; text: string }) {
return (
<section className="volt-grid border-b border-primary/25 px-4 py-14 md:px-6 md:py-20">
<div className="mx-auto max-w-7xl">
<Badge className="mb-6 bg-primary text-primary-foreground">{label}</Badge>
<h1 className="max-w-5xl text-4xl font-semibold uppercase leading-none md:text-7xl">
{title}
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-muted-foreground">{text}</p>
</div>
</section>
);
}

View File

@@ -0,0 +1,35 @@
import { ClipboardCheckIcon, FileTextIcon } from "lucide-react";
import { EnergyCalculator } from "@/features/energy-calculator/ui/energy-calculator";
import { documents } from "@/entities/site-content";
import { SectionHeader } from "@/shared/ui/section-header";
import { SiteShell } from "@/widgets/site-shell";
export function CalculatorPage() {
return (
<SiteShell>
<SectionHeader
label="Calculator"
title="Калькулятор продает не магию экономии, а понятную модель объекта"
text="Пользователь двигает счет, дневную долю потребления и резерв. На выходе получает ориентир по мощности, CAPEX, экономии, CO2 и окупаемости."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[1fr_380px]">
<EnergyCalculator />
<aside className="border border-primary/25 bg-card p-6">
<FileTextIcon className="size-9 text-primary" />
<h2 className="mt-8 text-3xl font-semibold uppercase leading-none">Что входит в инженерный отчет</h2>
<div className="mt-6 grid gap-3">
{documents.map((item) => (
<div key={item} className="flex items-center gap-3 border border-primary/20 bg-background/70 p-3 text-sm">
<ClipboardCheckIcon className="size-4 text-primary" />
<span>{item}</span>
</div>
))}
</div>
</aside>
</div>
</section>
</SiteShell>
);
}

View File

@@ -0,0 +1,61 @@
import { LeafIcon, PhoneCallIcon } from "lucide-react";
import { site } from "@/entities/site-content";
import { Button } from "@/shared/ui/button";
import { SectionHeader } from "@/shared/ui/section-header";
import { SiteShell } from "@/widgets/site-shell";
function ContactStat({ label, value }: { label: string; value: string }) {
return (
<div className="border border-accent/35 bg-accent/10 p-4">
<div className="text-xs uppercase text-muted-foreground">{label}</div>
<div className="mt-2 break-words font-semibold text-accent">{value}</div>
</div>
);
}
export function ContactsPage() {
return (
<SiteShell>
<SectionHeader
label="Contacts"
title="Запрос аудита начинается с вводных, которые нужны инженеру"
text="Контактная страница не просит просто оставить телефон. Она собирает счета, фото узлов, график работы и критичные нагрузки."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[1fr_430px]">
<div className="border border-primary/25 bg-card p-6">
<PhoneCallIcon className="size-10 text-primary" />
<h2 className="mt-10 text-4xl font-semibold uppercase leading-none">Инженерный бриф</h2>
<div className="mt-8 grid gap-3 md:grid-cols-2">
{[
"счет за 12 месяцев",
"адрес и тип объекта",
"фото кровли и щитовой",
"критичные линии",
"график работы",
"желательный срок запуска",
].map((item) => (
<div key={item} className="border border-primary/20 bg-background/70 p-4 text-sm">{item}</div>
))}
</div>
<div className="mt-8 grid gap-4 md:grid-cols-3">
<ContactStat label="email" value={site.email} />
<ContactStat label="phone" value={site.phone} />
<ContactStat label="desk" value="48 ч расчет" />
</div>
</div>
<aside className="border border-primary/25 bg-card p-6">
<LeafIcon className="size-10 text-primary" />
<h3 className="mt-10 text-3xl font-semibold uppercase leading-none">Что отправим после брифа</h3>
<p className="mt-5 leading-7 text-muted-foreground">
Предварительный диапазон мощности, экономику, список рисков и перечень данных
для полевого аудита. Это mock-форма шаблона, без реальной отправки.
</p>
<Button className="mt-8 w-full" size="lg">Запросить аудит</Button>
</aside>
</div>
</section>
</SiteShell>
);
}

View File

@@ -0,0 +1,47 @@
import { CheckCircle2Icon, ShieldCheckIcon } from "lucide-react";
import { financing } from "@/entities/site-content";
import { SectionHeader } from "@/shared/ui/section-header";
import { SiteShell } from "@/widgets/site-shell";
export function FinancingPage() {
return (
<SiteShell>
<SectionHeader
label="Financing"
title="Финансовая модель выбирается под горизонт владения объектом"
text="Одна и та же система может продаваться как CAPEX-проект, лизинг или сервисная модель. Страница помогает не смешивать эти сценарии."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-5 lg:grid-cols-3">
{financing.map((item) => (
<article key={item.title} className="border border-primary/25 bg-card p-6">
<ShieldCheckIcon className="size-9 text-primary" />
<h2 className="mt-10 text-4xl font-semibold uppercase leading-none">{item.title}</h2>
<div className="mt-5 grid grid-cols-2 gap-2 text-sm">
<div className="bg-background/70 p-3">
<div className="text-muted-foreground">аванс</div>
<div className="mt-1 font-semibold text-primary">{item.upfront}</div>
</div>
<div className="bg-background/70 p-3">
<div className="text-muted-foreground">горизонт</div>
<div className="mt-1 font-semibold text-primary">{item.term}</div>
</div>
</div>
<p className="mt-5 leading-7 text-muted-foreground">{item.text}</p>
<div className="mt-6 text-sm text-accent">{item.bestFor}</div>
<div className="mt-4 grid gap-2">
{item.points.map((point) => (
<div key={point} className="flex items-center gap-2 text-sm">
<CheckCircle2Icon className="size-4 text-primary" />
{point}
</div>
))}
</div>
</article>
))}
</div>
</section>
</SiteShell>
);
}

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

@@ -0,0 +1,196 @@
import Image from "next/image";
import Link from "next/link";
import {
BatteryChargingIcon,
CheckCircle2Icon,
GaugeIcon,
SunIcon,
ThermometerSunIcon,
} from "lucide-react";
import { auditMetrics, heroFacts, process, site, solutionStack } from "@/entities/site-content";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { SiteShell } from "@/widgets/site-shell";
function MetricStrip() {
return (
<section className="border-y border-primary/25 bg-card/40 px-4 py-4 md:px-6">
<div className="mx-auto grid max-w-7xl gap-3 md:grid-cols-4">
{auditMetrics.map((item) => (
<div key={item.label} className="border border-primary/25 bg-background/70 p-4">
<div className="text-xs uppercase text-muted-foreground">{item.label}</div>
<div className="mt-3 text-3xl font-semibold text-primary">{item.value}</div>
</div>
))}
</div>
</section>
);
}
function SystemNode({
icon,
title,
value,
detail,
}: {
icon: React.ReactNode;
title: string;
value: string;
detail: string;
}) {
return (
<div className="grid grid-cols-[40px_1fr_auto] items-center gap-3 border border-primary/20 bg-background/75 p-2.5">
<div className="grid size-10 place-items-center bg-primary text-primary-foreground">{icon}</div>
<div>
<div className="text-sm uppercase text-muted-foreground">{title}</div>
<div className="font-semibold">{detail}</div>
</div>
<div className="text-right text-lg font-semibold text-primary">{value}</div>
</div>
);
}
function EnergyBoard() {
return (
<div className="volt-panel overflow-hidden border border-primary/35 bg-card shadow-[0_0_60px_oklch(0.82_0.16_78_/_0.1)]">
<div className="grid gap-0 lg:grid-cols-[1.05fr_0.95fr]">
<div className="relative min-h-[260px] border-b border-primary/25 lg:border-b-0 lg:border-r">
<Image
src="https://images.unsplash.com/photo-1509391366360-2e959784a276?auto=format&fit=crop&w=1400&q=82"
alt="Солнечная станция на кровле промышленного объекта"
fill
priority
className="object-cover grayscale"
sizes="(min-width: 1024px) 42vw, 100vw"
/>
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/35 to-transparent" />
<div className="absolute bottom-4 left-4 right-4 grid gap-3 sm:grid-cols-3">
{heroFacts.map((item) => (
<div key={item.label} className="bg-background/85 p-3 backdrop-blur">
<div className="text-[11px] uppercase text-muted-foreground">{item.label}</div>
<div className="mt-1 text-xl font-semibold text-primary">{item.value}</div>
<div className="mt-1 text-xs text-muted-foreground">{item.detail}</div>
</div>
))}
</div>
</div>
<div className="volt-grid p-5">
<div className="flex items-start justify-between gap-4">
<div>
<div className="text-xs uppercase text-muted-foreground">object model</div>
<h2 className="mt-2 text-2xl font-semibold uppercase leading-none">Cold storage / 126 кВт пик</h2>
</div>
<span className="border border-primary/40 bg-primary/10 px-3 py-1 text-sm text-primary">live spec</span>
</div>
<div className="mt-6 grid gap-3">
<SystemNode icon={<SunIcon className="size-5" />} title="PV roof" value="212 кВт" detail="31 строка, 4 инвертора" />
<SystemNode icon={<BatteryChargingIcon className="size-5" />} title="Battery rack" value="320 кВт⋅ч" detail="резерв холода и IT" />
<SystemNode icon={<GaugeIcon className="size-5" />} title="Load panel" value="A/B priority" detail="переключение до 1 сек" />
<SystemNode icon={<ThermometerSunIcon className="size-5" />} title="Heat loop" value="COP 3.8" detail="буфер 2 000 литров" />
</div>
<div className="mt-6 border border-accent/40 bg-accent/10 p-4">
<div className="flex items-center justify-between gap-4 text-sm">
<span className="text-muted-foreground">прогноз OPEX после ввода</span>
<span className="font-semibold text-accent">-2.8 млн / год</span>
</div>
<div className="mt-3 h-2 bg-background">
<div className="h-full w-[68%] bg-accent" />
</div>
</div>
</div>
</div>
</div>
);
}
function StackCard({
item,
icon,
}: {
item: (typeof solutionStack)[number];
icon: React.ReactNode;
}) {
return (
<article className="border border-primary/25 bg-card p-5">
<div className="flex items-center justify-between gap-4">
<span className="grid size-11 place-items-center bg-primary text-primary-foreground">{icon}</span>
<span className="text-sm text-primary">{item.range}</span>
</div>
<h3 className="mt-8 text-3xl font-semibold uppercase leading-none">{item.title}</h3>
<p className="mt-4 leading-7 text-muted-foreground">{item.text}</p>
<div className="mt-6 grid gap-2">
{item.includes.map((point) => (
<div key={point} className="flex items-center gap-2 text-sm">
<CheckCircle2Icon className="size-4 text-primary" />
<span>{point}</span>
</div>
))}
</div>
</article>
);
}
function ProcessRail() {
return (
<section className="px-4 py-14 md:px-6">
<div className="mx-auto max-w-7xl">
<div className="mb-7 grid gap-4 md:grid-cols-[0.8fr_1.2fr] md:items-end">
<h2 className="text-4xl font-semibold uppercase leading-none md:text-6xl">Процесс без тумана в смете</h2>
<p className="max-w-2xl text-muted-foreground">
Шаблон продает не только панели и насосы. Он объясняет, какие исходные данные нужны,
где инженер проверяет объект и почему монтаж не должен ломать операционный режим.
</p>
</div>
<div className="grid gap-3 md:grid-cols-4">
{process.map((item) => (
<article key={item.step} className="border border-primary/25 bg-card p-5">
<div className="text-4xl font-semibold text-primary">{item.step}</div>
<h3 className="mt-8 text-xl font-semibold uppercase">{item.title}</h3>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
</article>
))}
</div>
</div>
</section>
);
}
export function HomePage() {
return (
<SiteShell>
<section className="volt-grid px-4 py-10 md:px-6 md:py-14">
<div className="mx-auto grid max-w-7xl gap-8 lg:grid-cols-[0.88fr_1.12fr] lg:items-center">
<div>
<Badge className="mb-6 bg-primary text-primary-foreground">{site.tagline}</Badge>
<h1 className="text-4xl font-semibold uppercase leading-none md:text-6xl">
Сначала расчет, потом оборудование
</h1>
<p className="mt-5 max-w-xl text-lg leading-8 text-muted-foreground">
Volthouse проектирует солнечную генерацию, тепловые насосы и батарейный резерв
для объектов, где ошибка в мощности превращается в дорогой простой.
</p>
<div className="mt-6 flex flex-wrap gap-3">
<Button asChild size="lg">
<Link href="/calculator">Рассчитать экономику</Link>
</Button>
<Button asChild variant="outline" size="lg">
<Link href="/projects">Смотреть кейсы</Link>
</Button>
</div>
</div>
<EnergyBoard />
</div>
</section>
<MetricStrip />
<section className="px-4 py-14 md:px-6">
<div className="mx-auto grid max-w-7xl gap-4 md:grid-cols-3">
<StackCard item={solutionStack[0]} icon={<SunIcon className="size-5" />} />
<StackCard item={solutionStack[1]} icon={<ThermometerSunIcon className="size-5" />} />
<StackCard item={solutionStack[2]} icon={<BatteryChargingIcon className="size-5" />} />
</div>
</section>
<ProcessRail />
</SiteShell>
);
}

View File

@@ -0,0 +1,55 @@
import { WrenchIcon } from "lucide-react";
import { maintenance } from "@/entities/site-content";
import { SectionHeader } from "@/shared/ui/section-header";
import { SiteShell } from "@/widgets/site-shell";
export function MaintenancePage() {
return (
<SiteShell>
<SectionHeader
label="Service"
title="Сервис встроен в продажу, потому что система живет после монтажа"
text="Страница отвечает на главный страх клиента: кто заметит деградацию, кто приедет на объект и как собственник увидит эффект."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[0.8fr_1.2fr]">
<div className="border border-primary/25 bg-card p-6">
<WrenchIcon className="size-10 text-primary" />
<h2 className="mt-10 text-4xl font-semibold uppercase leading-none">SLA console</h2>
<p className="mt-5 leading-7 text-muted-foreground">
Все события системы собираются в журнал: аварии, просадки выработки,
перегрев, падение COP, ошибки инверторов и ручные работы инженера.
</p>
<div className="mt-8 grid gap-3">
<div className="flex items-center justify-between border border-primary/20 bg-background/70 p-3">
<span className="text-muted-foreground">critical alert</span>
<span className="font-semibold text-primary">15 минут</span>
</div>
<div className="flex items-center justify-between border border-primary/20 bg-background/70 p-3">
<span className="text-muted-foreground">monthly report</span>
<span className="font-semibold text-primary">до 5 числа</span>
</div>
<div className="flex items-center justify-between border border-primary/20 bg-background/70 p-3">
<span className="text-muted-foreground">field visit</span>
<span className="font-semibold text-primary">по регламенту</span>
</div>
</div>
</div>
<div className="divide-y divide-primary/20 border border-primary/25 bg-card">
{maintenance.map((item, index) => (
<article key={item.title} className="grid gap-4 p-5 md:grid-cols-[92px_1fr_140px] md:items-center">
<div className="text-4xl font-semibold text-primary">0{index + 1}</div>
<div>
<h3 className="text-2xl font-semibold uppercase">{item.title}</h3>
<p className="mt-2 leading-7 text-muted-foreground">{item.text}</p>
</div>
<div className="border border-accent/35 bg-accent/10 p-3 text-sm text-accent">{item.response}</div>
</article>
))}
</div>
</div>
</section>
</SiteShell>
);
}

View File

@@ -0,0 +1,54 @@
import Image from "next/image";
import { projects } from "@/entities/site-content";
import { SectionHeader } from "@/shared/ui/section-header";
import { SiteShell } from "@/widgets/site-shell";
export function ProjectsPage() {
return (
<SiteShell>
<SectionHeader
label="Projects"
title="Кейсы показывают ограничение объекта, схему решения и измеримый итог"
text="Для инженерного сайта недостаточно красивой фотографии. Покупатель должен видеть мощность, проблему, срок и экономику."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-5 lg:grid-cols-3">
{projects.map((project) => (
<article key={project.title} className="overflow-hidden border border-primary/25 bg-card">
<div className="relative h-72">
<Image
src={project.image}
alt={project.title}
fill
className="object-cover grayscale"
sizes="(min-width: 1024px) 33vw, 100vw"
/>
<div className="absolute inset-0 bg-gradient-to-t from-background/90 via-background/20 to-transparent" />
<div className="absolute bottom-4 left-4 right-4">
<div className="text-sm text-muted-foreground">{project.location}</div>
<h2 className="mt-1 text-3xl font-semibold uppercase leading-none">{project.title}</h2>
</div>
</div>
<div className="p-5">
<div className="text-sm text-primary">{project.type}</div>
<p className="mt-4 min-h-20 leading-7 text-muted-foreground">{project.challenge}</p>
<div className="mt-6 border border-accent/35 bg-accent/10 p-4 text-xl font-semibold text-accent">
{project.result}
</div>
<div className="mt-4 grid grid-cols-3 gap-2">
{project.stats.map((stat) => (
<div key={stat.label} className="bg-background/75 p-3">
<div className="text-[11px] uppercase text-muted-foreground">{stat.label}</div>
<div className="mt-1 font-semibold">{stat.value}</div>
</div>
))}
</div>
</div>
</article>
))}
</div>
</section>
</SiteShell>
);
}

View File

@@ -0,0 +1,42 @@
import Link from "next/link";
import { ZapIcon } from "lucide-react";
import { navItems, site } from "@/entities/site-content";
import { Button } from "@/shared/ui/button";
export function SiteShell({ children }: { children: React.ReactNode }) {
return (
<main className="min-h-screen bg-background text-foreground">
<header className="sticky top-0 z-30 border-b border-primary/25 bg-background/90 backdrop-blur-xl">
<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-3 font-semibold uppercase">
<span className="grid size-9 place-items-center border border-primary/50 bg-primary text-primary-foreground">
<ZapIcon className="size-5" />
</span>
<span>{site.name}</span>
</Link>
<nav className="hidden items-center gap-5 text-sm text-muted-foreground lg:flex">
{navItems.map((item) => (
<Link key={item.href} href={item.href} className="hover:text-primary">
{item.label}
</Link>
))}
</nav>
<Button asChild size="sm">
<Link href="/contacts">Полевой аудит</Link>
</Button>
</div>
</header>
{children}
<footer className="border-t border-primary/25 px-4 py-8 md:px-6">
<div className="mx-auto flex max-w-7xl flex-col gap-4 text-sm text-muted-foreground md:flex-row md:items-center md:justify-between">
<div>{site.name} - проектирование, монтаж и сервис энергосистем.</div>
<div className="flex flex-wrap gap-4">
<span>{site.city}</span>
<span>{site.email}</span>
</div>
</div>
</footer>
</main>
);
}

View File

@@ -0,0 +1,46 @@
import { CheckCircle2Icon } from "lucide-react";
import { solutions } from "@/entities/site-content";
import { Badge } from "@/shared/ui/badge";
import { SectionHeader } from "@/shared/ui/section-header";
import { SiteShell } from "@/widgets/site-shell";
export function SolutionsPage() {
return (
<SiteShell>
<SectionHeader
label="Solutions"
title="Решения разделены не по трендам, а по инженерным ограничениям"
text="Каждая карточка показывает тип объекта, диапазон мощности, состав работ и документ, который получает клиент перед закупкой."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-5 md:grid-cols-2">
{solutions.map((solution) => (
<article key={solution.title} className="border border-primary/25 bg-card p-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<Badge variant="outline" className="border-primary/40 text-primary">
{solution.power}
</Badge>
<span className="text-sm text-muted-foreground">{solution.fit}</span>
</div>
<h2 className="mt-10 text-4xl font-semibold uppercase leading-none">{solution.title}</h2>
<p className="mt-5 leading-7 text-muted-foreground">{solution.text}</p>
<div className="mt-7 border border-accent/35 bg-accent/10 p-4">
<div className="text-xs uppercase text-muted-foreground">deliverable</div>
<div className="mt-2 font-semibold text-accent">{solution.deliverable}</div>
</div>
<div className="mt-6 grid gap-2">
{solution.includes.map((point) => (
<div key={point} className="flex items-center gap-2 text-sm">
<CheckCircle2Icon className="size-4 text-primary" />
{point}
</div>
))}
</div>
</article>
))}
</div>
</section>
</SiteShell>
);
}

View File

@@ -1,531 +0,0 @@
import Image from "next/image";
import Link from "next/link";
import {
BatteryChargingIcon,
CheckCircle2Icon,
ClipboardCheckIcon,
FileTextIcon,
GaugeIcon,
LeafIcon,
PhoneCallIcon,
ShieldCheckIcon,
SunIcon,
ThermometerSunIcon,
WrenchIcon,
ZapIcon,
} from "lucide-react";
import { EnergyCalculator } from "@/features/energy-calculator/ui/energy-calculator";
import {
auditMetrics,
documents,
financing,
heroFacts,
maintenance,
navItems,
process,
projects,
site,
solutions,
solutionStack,
} from "@/entities/site-content";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
function Shell({ children }: { children: React.ReactNode }) {
return (
<main className="min-h-screen bg-background text-foreground">
<header className="sticky top-0 z-30 border-b border-primary/25 bg-background/90 backdrop-blur-xl">
<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-3 font-semibold uppercase">
<span className="grid size-9 place-items-center border border-primary/50 bg-primary text-primary-foreground">
<ZapIcon className="size-5" />
</span>
<span>{site.name}</span>
</Link>
<nav className="hidden items-center gap-5 text-sm text-muted-foreground lg:flex">
{navItems.map((item) => (
<Link key={item.href} href={item.href} className="hover:text-primary">
{item.label}
</Link>
))}
</nav>
<Button asChild size="sm">
<Link href="/contacts">Полевой аудит</Link>
</Button>
</div>
</header>
{children}
<footer className="border-t border-primary/25 px-4 py-8 md:px-6">
<div className="mx-auto flex max-w-7xl flex-col gap-4 text-sm text-muted-foreground md:flex-row md:items-center md:justify-between">
<div>{site.name} - проектирование, монтаж и сервис энергосистем.</div>
<div className="flex flex-wrap gap-4">
<span>{site.city}</span>
<span>{site.email}</span>
</div>
</div>
</footer>
</main>
);
}
function SectionHeader({ label, title, text }: { label: string; title: string; text: string }) {
return (
<section className="volt-grid border-b border-primary/25 px-4 py-14 md:px-6 md:py-20">
<div className="mx-auto max-w-7xl">
<Badge className="mb-6 bg-primary text-primary-foreground">{label}</Badge>
<h1 className="max-w-5xl text-4xl font-semibold uppercase leading-none md:text-7xl">
{title}
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-muted-foreground">{text}</p>
</div>
</section>
);
}
function MetricStrip() {
return (
<section className="border-y border-primary/25 bg-card/40 px-4 py-4 md:px-6">
<div className="mx-auto grid max-w-7xl gap-3 md:grid-cols-4">
{auditMetrics.map((item) => (
<div key={item.label} className="border border-primary/25 bg-background/70 p-4">
<div className="text-xs uppercase text-muted-foreground">{item.label}</div>
<div className="mt-3 text-3xl font-semibold text-primary">{item.value}</div>
</div>
))}
</div>
</section>
);
}
function EnergyBoard() {
return (
<div className="volt-panel overflow-hidden border border-primary/35 bg-card shadow-[0_0_60px_oklch(0.82_0.16_78_/_0.1)]">
<div className="grid gap-0 lg:grid-cols-[1.05fr_0.95fr]">
<div className="relative min-h-[260px] border-b border-primary/25 lg:border-b-0 lg:border-r">
<Image
src="https://images.unsplash.com/photo-1509391366360-2e959784a276?auto=format&fit=crop&w=1400&q=82"
alt="Солнечная станция на кровле промышленного объекта"
fill
priority
className="object-cover grayscale"
sizes="(min-width: 1024px) 42vw, 100vw"
/>
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/35 to-transparent" />
<div className="absolute bottom-4 left-4 right-4 grid gap-3 sm:grid-cols-3">
{heroFacts.map((item) => (
<div key={item.label} className="bg-background/85 p-3 backdrop-blur">
<div className="text-[11px] uppercase text-muted-foreground">{item.label}</div>
<div className="mt-1 text-xl font-semibold text-primary">{item.value}</div>
<div className="mt-1 text-xs text-muted-foreground">{item.detail}</div>
</div>
))}
</div>
</div>
<div className="volt-grid p-5">
<div className="flex items-start justify-between gap-4">
<div>
<div className="text-xs uppercase text-muted-foreground">object model</div>
<h2 className="mt-2 text-2xl font-semibold uppercase leading-none">Cold storage / 126 кВт пик</h2>
</div>
<span className="border border-primary/40 bg-primary/10 px-3 py-1 text-sm text-primary">live spec</span>
</div>
<div className="mt-6 grid gap-3">
<SystemNode icon={<SunIcon className="size-5" />} title="PV roof" value="212 кВт" detail="31 строка, 4 инвертора" />
<SystemNode icon={<BatteryChargingIcon className="size-5" />} title="Battery rack" value="320 кВт⋅ч" detail="резерв холода и IT" />
<SystemNode icon={<GaugeIcon className="size-5" />} title="Load panel" value="A/B priority" detail="переключение до 1 сек" />
<SystemNode icon={<ThermometerSunIcon className="size-5" />} title="Heat loop" value="COP 3.8" detail="буфер 2 000 литров" />
</div>
<div className="mt-6 border border-accent/40 bg-accent/10 p-4">
<div className="flex items-center justify-between gap-4 text-sm">
<span className="text-muted-foreground">прогноз OPEX после ввода</span>
<span className="font-semibold text-accent">-2.8 млн / год</span>
</div>
<div className="mt-3 h-2 bg-background">
<div className="h-full w-[68%] bg-accent" />
</div>
</div>
</div>
</div>
</div>
);
}
function SystemNode({
icon,
title,
value,
detail,
}: {
icon: React.ReactNode;
title: string;
value: string;
detail: string;
}) {
return (
<div className="grid grid-cols-[40px_1fr_auto] items-center gap-3 border border-primary/20 bg-background/75 p-2.5">
<div className="grid size-10 place-items-center bg-primary text-primary-foreground">{icon}</div>
<div>
<div className="text-sm uppercase text-muted-foreground">{title}</div>
<div className="font-semibold">{detail}</div>
</div>
<div className="text-right text-lg font-semibold text-primary">{value}</div>
</div>
);
}
function StackCard({
item,
icon,
}: {
item: (typeof solutionStack)[number];
icon: React.ReactNode;
}) {
return (
<article className="border border-primary/25 bg-card p-5">
<div className="flex items-center justify-between gap-4">
<span className="grid size-11 place-items-center bg-primary text-primary-foreground">{icon}</span>
<span className="text-sm text-primary">{item.range}</span>
</div>
<h3 className="mt-8 text-3xl font-semibold uppercase leading-none">{item.title}</h3>
<p className="mt-4 leading-7 text-muted-foreground">{item.text}</p>
<div className="mt-6 grid gap-2">
{item.includes.map((point) => (
<div key={point} className="flex items-center gap-2 text-sm">
<CheckCircle2Icon className="size-4 text-primary" />
<span>{point}</span>
</div>
))}
</div>
</article>
);
}
function ProcessRail() {
return (
<section className="px-4 py-14 md:px-6">
<div className="mx-auto max-w-7xl">
<div className="mb-7 grid gap-4 md:grid-cols-[0.8fr_1.2fr] md:items-end">
<h2 className="text-4xl font-semibold uppercase leading-none md:text-6xl">Процесс без тумана в смете</h2>
<p className="max-w-2xl text-muted-foreground">
Шаблон продает не только панели и насосы. Он объясняет, какие исходные данные нужны,
где инженер проверяет объект и почему монтаж не должен ломать операционный режим.
</p>
</div>
<div className="grid gap-3 md:grid-cols-4">
{process.map((item) => (
<article key={item.step} className="border border-primary/25 bg-card p-5">
<div className="text-4xl font-semibold text-primary">{item.step}</div>
<h3 className="mt-8 text-xl font-semibold uppercase">{item.title}</h3>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
</article>
))}
</div>
</div>
</section>
);
}
export function HomePage() {
return (
<Shell>
<section className="volt-grid px-4 py-10 md:px-6 md:py-14">
<div className="mx-auto grid max-w-7xl gap-8 lg:grid-cols-[0.88fr_1.12fr] lg:items-center">
<div>
<Badge className="mb-6 bg-primary text-primary-foreground">{site.tagline}</Badge>
<h1 className="text-4xl font-semibold uppercase leading-none md:text-6xl">
Сначала расчет, потом оборудование
</h1>
<p className="mt-5 max-w-xl text-lg leading-8 text-muted-foreground">
Volthouse проектирует солнечную генерацию, тепловые насосы и батарейный резерв
для объектов, где ошибка в мощности превращается в дорогой простой.
</p>
<div className="mt-6 flex flex-wrap gap-3">
<Button asChild size="lg">
<Link href="/calculator">Рассчитать экономику</Link>
</Button>
<Button asChild variant="outline" size="lg">
<Link href="/projects">Смотреть кейсы</Link>
</Button>
</div>
</div>
<EnergyBoard />
</div>
</section>
<MetricStrip />
<section className="px-4 py-14 md:px-6">
<div className="mx-auto grid max-w-7xl gap-4 md:grid-cols-3">
<StackCard item={solutionStack[0]} icon={<SunIcon className="size-5" />} />
<StackCard item={solutionStack[1]} icon={<ThermometerSunIcon className="size-5" />} />
<StackCard item={solutionStack[2]} icon={<BatteryChargingIcon className="size-5" />} />
</div>
</section>
<ProcessRail />
</Shell>
);
}
export function SolutionsPage() {
return (
<Shell>
<SectionHeader
label="Solutions"
title="Решения разделены не по трендам, а по инженерным ограничениям"
text="Каждая карточка показывает тип объекта, диапазон мощности, состав работ и документ, который получает клиент перед закупкой."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-5 md:grid-cols-2">
{solutions.map((solution) => (
<article key={solution.title} className="border border-primary/25 bg-card p-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<Badge variant="outline" className="border-primary/40 text-primary">
{solution.power}
</Badge>
<span className="text-sm text-muted-foreground">{solution.fit}</span>
</div>
<h2 className="mt-10 text-4xl font-semibold uppercase leading-none">{solution.title}</h2>
<p className="mt-5 leading-7 text-muted-foreground">{solution.text}</p>
<div className="mt-7 border border-accent/35 bg-accent/10 p-4">
<div className="text-xs uppercase text-muted-foreground">deliverable</div>
<div className="mt-2 font-semibold text-accent">{solution.deliverable}</div>
</div>
<div className="mt-6 grid gap-2">
{solution.includes.map((point) => (
<div key={point} className="flex items-center gap-2 text-sm">
<CheckCircle2Icon className="size-4 text-primary" />
{point}
</div>
))}
</div>
</article>
))}
</div>
</section>
</Shell>
);
}
export function CalculatorPage() {
return (
<Shell>
<SectionHeader
label="Calculator"
title="Калькулятор продает не магию экономии, а понятную модель объекта"
text="Пользователь двигает счет, дневную долю потребления и резерв. На выходе получает ориентир по мощности, CAPEX, экономии, CO2 и окупаемости."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[1fr_380px]">
<EnergyCalculator />
<aside className="border border-primary/25 bg-card p-6">
<FileTextIcon className="size-9 text-primary" />
<h2 className="mt-8 text-3xl font-semibold uppercase leading-none">Что входит в инженерный отчет</h2>
<div className="mt-6 grid gap-3">
{documents.map((item) => (
<div key={item} className="flex items-center gap-3 border border-primary/20 bg-background/70 p-3 text-sm">
<ClipboardCheckIcon className="size-4 text-primary" />
<span>{item}</span>
</div>
))}
</div>
</aside>
</div>
</section>
</Shell>
);
}
export function ProjectsPage() {
return (
<Shell>
<SectionHeader
label="Projects"
title="Кейсы показывают ограничение объекта, схему решения и измеримый итог"
text="Для инженерного сайта недостаточно красивой фотографии. Покупатель должен видеть мощность, проблему, срок и экономику."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-5 lg:grid-cols-3">
{projects.map((project) => (
<article key={project.title} className="overflow-hidden border border-primary/25 bg-card">
<div className="relative h-72">
<Image
src={project.image}
alt={project.title}
fill
className="object-cover grayscale"
sizes="(min-width: 1024px) 33vw, 100vw"
/>
<div className="absolute inset-0 bg-gradient-to-t from-background/90 via-background/20 to-transparent" />
<div className="absolute bottom-4 left-4 right-4">
<div className="text-sm text-muted-foreground">{project.location}</div>
<h2 className="mt-1 text-3xl font-semibold uppercase leading-none">{project.title}</h2>
</div>
</div>
<div className="p-5">
<div className="text-sm text-primary">{project.type}</div>
<p className="mt-4 min-h-20 leading-7 text-muted-foreground">{project.challenge}</p>
<div className="mt-6 border border-accent/35 bg-accent/10 p-4 text-xl font-semibold text-accent">
{project.result}
</div>
<div className="mt-4 grid grid-cols-3 gap-2">
{project.stats.map((stat) => (
<div key={stat.label} className="bg-background/75 p-3">
<div className="text-[11px] uppercase text-muted-foreground">{stat.label}</div>
<div className="mt-1 font-semibold">{stat.value}</div>
</div>
))}
</div>
</div>
</article>
))}
</div>
</section>
</Shell>
);
}
export function MaintenancePage() {
return (
<Shell>
<SectionHeader
label="Service"
title="Сервис встроен в продажу, потому что система живет после монтажа"
text="Страница отвечает на главный страх клиента: кто заметит деградацию, кто приедет на объект и как собственник увидит эффект."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[0.8fr_1.2fr]">
<div className="border border-primary/25 bg-card p-6">
<WrenchIcon className="size-10 text-primary" />
<h2 className="mt-10 text-4xl font-semibold uppercase leading-none">SLA console</h2>
<p className="mt-5 leading-7 text-muted-foreground">
Все события системы собираются в журнал: аварии, просадки выработки,
перегрев, падение COP, ошибки инверторов и ручные работы инженера.
</p>
<div className="mt-8 grid gap-3">
<div className="flex items-center justify-between border border-primary/20 bg-background/70 p-3">
<span className="text-muted-foreground">critical alert</span>
<span className="font-semibold text-primary">15 минут</span>
</div>
<div className="flex items-center justify-between border border-primary/20 bg-background/70 p-3">
<span className="text-muted-foreground">monthly report</span>
<span className="font-semibold text-primary">до 5 числа</span>
</div>
<div className="flex items-center justify-between border border-primary/20 bg-background/70 p-3">
<span className="text-muted-foreground">field visit</span>
<span className="font-semibold text-primary">по регламенту</span>
</div>
</div>
</div>
<div className="divide-y divide-primary/20 border border-primary/25 bg-card">
{maintenance.map((item, index) => (
<article key={item.title} className="grid gap-4 p-5 md:grid-cols-[92px_1fr_140px] md:items-center">
<div className="text-4xl font-semibold text-primary">0{index + 1}</div>
<div>
<h3 className="text-2xl font-semibold uppercase">{item.title}</h3>
<p className="mt-2 leading-7 text-muted-foreground">{item.text}</p>
</div>
<div className="border border-accent/35 bg-accent/10 p-3 text-sm text-accent">{item.response}</div>
</article>
))}
</div>
</div>
</section>
</Shell>
);
}
export function FinancingPage() {
return (
<Shell>
<SectionHeader
label="Financing"
title="Финансовая модель выбирается под горизонт владения объектом"
text="Одна и та же система может продаваться как CAPEX-проект, лизинг или сервисная модель. Страница помогает не смешивать эти сценарии."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-5 lg:grid-cols-3">
{financing.map((item) => (
<article key={item.title} className="border border-primary/25 bg-card p-6">
<ShieldCheckIcon className="size-9 text-primary" />
<h2 className="mt-10 text-4xl font-semibold uppercase leading-none">{item.title}</h2>
<div className="mt-5 grid grid-cols-2 gap-2 text-sm">
<div className="bg-background/70 p-3">
<div className="text-muted-foreground">аванс</div>
<div className="mt-1 font-semibold text-primary">{item.upfront}</div>
</div>
<div className="bg-background/70 p-3">
<div className="text-muted-foreground">горизонт</div>
<div className="mt-1 font-semibold text-primary">{item.term}</div>
</div>
</div>
<p className="mt-5 leading-7 text-muted-foreground">{item.text}</p>
<div className="mt-6 text-sm text-accent">{item.bestFor}</div>
<div className="mt-4 grid gap-2">
{item.points.map((point) => (
<div key={point} className="flex items-center gap-2 text-sm">
<CheckCircle2Icon className="size-4 text-primary" />
{point}
</div>
))}
</div>
</article>
))}
</div>
</section>
</Shell>
);
}
export function ContactsPage() {
return (
<Shell>
<SectionHeader
label="Contacts"
title="Запрос аудита начинается с вводных, которые нужны инженеру"
text="Контактная страница не просит просто оставить телефон. Она собирает счета, фото узлов, график работы и критичные нагрузки."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[1fr_430px]">
<div className="border border-primary/25 bg-card p-6">
<PhoneCallIcon className="size-10 text-primary" />
<h2 className="mt-10 text-4xl font-semibold uppercase leading-none">Инженерный бриф</h2>
<div className="mt-8 grid gap-3 md:grid-cols-2">
{[
"счет за 12 месяцев",
"адрес и тип объекта",
"фото кровли и щитовой",
"критичные линии",
"график работы",
"желательный срок запуска",
].map((item) => (
<div key={item} className="border border-primary/20 bg-background/70 p-4 text-sm">{item}</div>
))}
</div>
<div className="mt-8 grid gap-4 md:grid-cols-3">
<ContactStat label="email" value={site.email} />
<ContactStat label="phone" value={site.phone} />
<ContactStat label="desk" value="48 ч расчет" />
</div>
</div>
<aside className="border border-primary/25 bg-card p-6">
<LeafIcon className="size-10 text-primary" />
<h3 className="mt-10 text-3xl font-semibold uppercase leading-none">Что отправим после брифа</h3>
<p className="mt-5 leading-7 text-muted-foreground">
Предварительный диапазон мощности, экономику, список рисков и перечень данных
для полевого аудита. Это mock-форма шаблона, без реальной отправки.
</p>
<Button className="mt-8 w-full" size="lg">Запросить аудит</Button>
</aside>
</div>
</section>
</Shell>
);
}
function ContactStat({ label, value }: { label: string; value: string }) {
return (
<div className="border border-accent/35 bg-accent/10 p-4">
<div className="text-xs uppercase text-muted-foreground">{label}</div>
<div className="mt-2 break-words font-semibold text-accent">{value}</div>
</div>
);
}