feat: split big file and update agents.md
This commit is contained in:
51
AGENTS.md
51
AGENTS.md
@@ -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`.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CalculatorPage } from "@/widgets/template-ui";
|
||||
import { CalculatorPage } from "@/widgets/calculator-page";
|
||||
|
||||
export default function Page() {
|
||||
return <CalculatorPage />;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ContactsPage } from "@/widgets/template-ui";
|
||||
import { ContactsPage } from "@/widgets/contacts-page";
|
||||
|
||||
export default function Page() {
|
||||
return <ContactsPage />;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FinancingPage } from "@/widgets/template-ui";
|
||||
import { FinancingPage } from "@/widgets/financing-page";
|
||||
|
||||
export default function Page() {
|
||||
return <FinancingPage />;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MaintenancePage } from "@/widgets/template-ui";
|
||||
import { MaintenancePage } from "@/widgets/maintenance-page";
|
||||
|
||||
export default function Page() {
|
||||
return <MaintenancePage />;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HomePage } from "@/widgets/template-ui";
|
||||
import { HomePage } from "@/widgets/home-page";
|
||||
|
||||
export default function Page() {
|
||||
return <HomePage />;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ProjectsPage } from "@/widgets/template-ui";
|
||||
import { ProjectsPage } from "@/widgets/projects-page";
|
||||
|
||||
export default function Page() {
|
||||
return <ProjectsPage />;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SolutionsPage } from "@/widgets/template-ui";
|
||||
import { SolutionsPage } from "@/widgets/solutions-page";
|
||||
|
||||
export default function Page() {
|
||||
return <SolutionsPage />;
|
||||
|
||||
15
src/shared/ui/section-header.tsx
Normal file
15
src/shared/ui/section-header.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
35
src/widgets/calculator-page.tsx
Normal file
35
src/widgets/calculator-page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
61
src/widgets/contacts-page.tsx
Normal file
61
src/widgets/contacts-page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
47
src/widgets/financing-page.tsx
Normal file
47
src/widgets/financing-page.tsx
Normal 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
196
src/widgets/home-page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
55
src/widgets/maintenance-page.tsx
Normal file
55
src/widgets/maintenance-page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
54
src/widgets/projects-page.tsx
Normal file
54
src/widgets/projects-page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
42
src/widgets/site-shell.tsx
Normal file
42
src/widgets/site-shell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
46
src/widgets/solutions-page.tsx
Normal file
46
src/widgets/solutions-page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user