feat: split big file and update agents.md
This commit is contained in:
62
AGENTS.md
62
AGENTS.md
@@ -5,7 +5,7 @@ Northline Clinic — planned care clinic template: пациентский мар
|
||||
## Project Specifics
|
||||
|
||||
- Source content lives in `src/entities/site-content.ts`; keep visible copy there or directly in route JSX.
|
||||
- `src/app` is routing only; page composition belongs to `src/widgets/template-ui.tsx`.
|
||||
- `src/app` — только route wrappers; композиция каждой страницы живёт в отдельном widget (`src/widgets/<page>-page.tsx`). Header/footer — `src/widgets/site-shell.tsx`. См. File Map.
|
||||
- Keep cards at 8px radius or less and preserve the selected palette in `src/app/globals.css`.
|
||||
- This is a static frontend template: do not add real payments, auth, persistence, external API calls, or backend contracts without an explicit product request.
|
||||
- Keep the domain specific: planned care, triage, preparation, diagnostic route, result review, insurance/privacy and non-emergency boundaries. Do not flatten it into generic wellness cards.
|
||||
@@ -16,3 +16,63 @@ Northline Clinic — planned care clinic template: пациентский мар
|
||||
|
||||
- Кириллица обязательна для видимого текста и выбранных Google Fonts.
|
||||
- При AI-правках сохраняйте доменные блоки этого шаблона: они отличают проект от generic landing.
|
||||
|
||||
## Design System
|
||||
|
||||
Источник токенов — `src/app/globals.css` (`@theme inline` + `:root`/`.dark`). Шрифты: заголовки и `.font-display` — **Manrope** (`--font-display`), body — **Source Sans 3** (`--font-sans`); mono замаплен на display. Работай через семантические классы Tailwind (`bg-primary`, `text-muted-foreground`, `border-border`, `border-primary/30`), не хардкодь hex/oklch.
|
||||
|
||||
Личность: **calm clinical / planned care** — тёплый кремово-бежевый фон (low-chroma), приглушённый сине-зелёный teal `primary` (oklch 0.46 0.07 173) как доверие и навигация, мягкий мятный `secondary`, тёплый терракотовый `accent` (oklch 0.72 0.12 34) для редких сигналов. Текст — тёмный teal-ink, а не чистый чёрный. Никакой «больничной» стерильности и никакого агрессивного маркетинга — спокойная, аккуратная среда.
|
||||
|
||||
| Роль | Light | Характер |
|
||||
|---|---|---|
|
||||
| `background` | тёплый кремово-бежевый (0.976) | основной фон страниц |
|
||||
| `foreground` | тёмный teal-ink (0.18) | текст |
|
||||
| `primary` | приглушённый teal | доверие: иконки, CTA, бейджи (`text-primary`), eyebrow, `border-primary/30` |
|
||||
| `secondary` | мягкий мятный | вторичные бейджи (теги маршрутов) |
|
||||
| `accent` | тёплый терракот | редкий сигнал (используется сдержанно) |
|
||||
| `muted` | тёплый серо-бежевый | чипы, фон-плашки, вторичный текст |
|
||||
| `card` | почти белый тёплый (0.995) | панели `ClinicalPanel` |
|
||||
| `border` | мягкий тёплый серый | тонкие границы (`border border-border`) |
|
||||
|
||||
Узнаваемые приёмы (держи их, это и есть «лицо» проекта):
|
||||
- **Мягкие, но не круглые углы:** `--radius` = 0.5rem; почти всё — `rounded-md`. Спокойная гладкость без пузырей.
|
||||
- **`ClinicalPanel` как базовый блок:** `rounded-md border border-border bg-card shadow-[0_1px_0_rgba(24,74,68,0.05)]` — почти плоская карточка с едва заметной teal-линией снизу. Это основной строительный кирпич контента.
|
||||
- **Тонкие границы, плоскость:** `border border-border` (1px), без жёстких теней и offset-shadow. Глубина — минимальная.
|
||||
- **Цветная семантика teal:** иконки, цифры-метрики, eyebrow и активные подписи — `text-primary`; границы-акценты — `border-primary/30`.
|
||||
- **Типографика:** заголовки — `font-semibold` (не black), `text-5xl`/`text-6xl`, спокойный `leading-tight`/`leading-[1.04]`; eyebrow — `uppercase tracking-[0.16em] text-primary`.
|
||||
- **Safety-блоки:** предупреждения о неэкстренном характере — `border-amber-200 bg-amber-50 text-amber-900/950` (единственное место с янтарём, намеренно).
|
||||
- **Фон-фактура:** `.clinic-soft-grid` — мягкая teal/терракотовая сетка под hero с маской снизу; контент в `Container` (`max-w-[1320px]`), фото — `object-cover` БЕЗ grayscale (живые, тёплые снимки).
|
||||
|
||||
Do / Don't:
|
||||
- **Do:** держи спокойную кремово-teal палитру, плоские `ClinicalPanel`, тонкие границы, доменный язык (маршрут, подготовка, координатор, неэкстренный формат), amber только для safety.
|
||||
- **Don't:** яркие/насыщенные цвета, жёсткие тени, grayscale-фото, `font-black`, агрессивные CTA, медицинские обещания/диагнозы — это ломает доверительную клиническую личность.
|
||||
|
||||
## File Map
|
||||
|
||||
| Route | Widget |
|
||||
|---|---|
|
||||
| `/` | `src/widgets/home-page.tsx` (`HomePage`) |
|
||||
| `/services` | `src/widgets/services-page.tsx` (`ServicesPage`) |
|
||||
| `/specialists` | `src/widgets/specialists-page.tsx` (`SpecialistsPage`) |
|
||||
| `/booking` | `src/widgets/booking-page.tsx` (`BookingPage`) |
|
||||
| `/patient-info` | `src/widgets/patient-info-page.tsx` (`PatientInfoPage`) |
|
||||
| `/contacts` | `src/widgets/contacts-page.tsx` (`ContactsPage`) |
|
||||
|
||||
Переиспользуемые блоки:
|
||||
- `src/widgets/site-shell.tsx` — `SiteHeader` + `SiteFooter` (обёртка всех страниц через `app/layout.tsx`).
|
||||
- `src/widgets/patient-journey.tsx` — `PatientJourney` (Home, Booking).
|
||||
- `src/widgets/featured-grid.tsx` — `FeaturedGrid` (Home, Services).
|
||||
- `src/widgets/specialist-schedule.tsx` — `SpecialistSchedule` (Home, Booking, Specialists).
|
||||
- `src/widgets/patient-checklist.tsx` — `PatientChecklist` (Home, Patient info, Booking).
|
||||
- `src/widgets/facility-board.tsx` — `FacilityBoard` (Home, Contacts).
|
||||
- `src/widgets/safety-notes.tsx` — `SafetyNotes` (Home, Patient info, Booking).
|
||||
- `src/widgets/clinical-disclaimer.tsx` — `ClinicalDisclaimer` (Home, Patient info, Booking, Services).
|
||||
- `src/widgets/metric-strip.tsx` — `MetricStrip` (переиспользуемый блок-кит, готов к композиции).
|
||||
- `src/shared/ui/container.tsx` — `Container` (контентная обёртка `max-w-[1320px]`, база всех секций).
|
||||
- `src/shared/ui/clinical-panel.tsx` — `ClinicalPanel` (базовая карточка-панель).
|
||||
- `src/shared/ui/section-header.tsx` — `SectionHeader` (eyebrow + заголовок + текст секции).
|
||||
- `src/shared/ui/inner-hero.tsx` — `InnerHero` (заголовочная секция внутренних страниц).
|
||||
- `src/shared/ui/icon-cards.tsx` — `IconCards` (Contacts, Services).
|
||||
- `src/shared/ui/info-columns.tsx` — `InfoColumns`, `src/shared/ui/split-story.tsx` — `SplitStory` (переиспользуемые presentational-блоки-кит).
|
||||
|
||||
Одноразовые блоки колоцированы со своей страницей: `PageHero`/`TestimonialBand` в `home-page.tsx`, `ServiceMatrix`/`PricingTiles` в `services-page.tsx`, `DoctorGrid`/`CtaPanel` в `specialists-page.tsx`, `ReservationForm` в `booking-page.tsx`, `InsurancePanel` в `patient-info-page.tsx`, `ContactForm` в `contacts-page.tsx`.
|
||||
|
||||
@@ -1,29 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ClinicalDisclaimer,
|
||||
InnerHero,
|
||||
PatientChecklist,
|
||||
PatientJourney,
|
||||
ReservationForm,
|
||||
SafetyNotes,
|
||||
SpecialistSchedule,
|
||||
} from "@/widgets/template-ui";
|
||||
import { BookingPage } from "@/widgets/booking-page";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<InnerHero
|
||||
eyebrow="Booking"
|
||||
title="Запись как triage: цель визита, подготовка и неэкстренный контекст"
|
||||
text="Форма не отправляет данные наружу. Она показывает UI будущей записи: причина обращения, слот, документы и предупреждение о плановом формате."
|
||||
/>
|
||||
<SpecialistSchedule />
|
||||
<ReservationForm />
|
||||
<PatientJourney />
|
||||
<PatientChecklist />
|
||||
<SafetyNotes />
|
||||
<ClinicalDisclaimer />
|
||||
</>
|
||||
);
|
||||
return <BookingPage />;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { ContactForm, FacilityBoard, IconCards, InnerHero } from "@/widgets/template-ui";
|
||||
import { contactCards } from "@/entities/site-content";
|
||||
import { ContactsPage } from "@/widgets/contacts-page";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<InnerHero
|
||||
eyebrow="Contacts"
|
||||
title="Адрес, часы, лабораторное окно и связь с координатором"
|
||||
text="Контактная страница клиники должна помогать пациенту приехать подготовленным: адрес, время, документы, способ связи и понятное ожидание ответа."
|
||||
/>
|
||||
<IconCards items={contactCards} />
|
||||
<FacilityBoard />
|
||||
<ContactForm />
|
||||
</>
|
||||
);
|
||||
return <ContactsPage />;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Manrope, Source_Sans_3 } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/shared/hooks/theme-provider";
|
||||
import { ThemeMessageListener } from "@/shared/hooks/theme-message-listener";
|
||||
import { SiteHeader, SiteFooter } from "@/widgets/template-ui";
|
||||
import { SiteHeader, SiteFooter } from "@/widgets/site-shell";
|
||||
|
||||
const display = Manrope({
|
||||
variable: "--font-display",
|
||||
|
||||
@@ -1,33 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ClinicalDisclaimer,
|
||||
FacilityBoard,
|
||||
FeaturedGrid,
|
||||
PageHero,
|
||||
PatientChecklist,
|
||||
PatientJourney,
|
||||
SafetyNotes,
|
||||
SpecialistSchedule,
|
||||
TestimonialBand,
|
||||
} from "@/widgets/template-ui";
|
||||
import { HomePage } from "@/widgets/home-page";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<PageHero />
|
||||
<PatientJourney />
|
||||
<FeaturedGrid
|
||||
eyebrow="Care programs"
|
||||
title="Маршруты, которые продают порядок, а не обещания"
|
||||
text="Каждая услуга показывает формат, подготовку, состав маршрута и безопасный next step для пациента."
|
||||
/>
|
||||
<FacilityBoard />
|
||||
<SpecialistSchedule />
|
||||
<PatientChecklist />
|
||||
<SafetyNotes />
|
||||
<TestimonialBand />
|
||||
<ClinicalDisclaimer />
|
||||
</>
|
||||
);
|
||||
return <HomePage />;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { ClinicalDisclaimer, InnerHero, InsurancePanel, PatientChecklist, SafetyNotes } from "@/widgets/template-ui";
|
||||
import { PatientInfoPage } from "@/widgets/patient-info-page";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<InnerHero
|
||||
eyebrow="Patient info"
|
||||
title="Документы, страховые вопросы, результаты и правила планового визита"
|
||||
text="Сервисная страница закрывает вопросы до записи: что взять, как передаются заключения, почему форма не заменяет консультацию и где проходит граница экстренной помощи."
|
||||
/>
|
||||
<PatientChecklist />
|
||||
<InsurancePanel />
|
||||
<SafetyNotes />
|
||||
<ClinicalDisclaimer />
|
||||
</>
|
||||
);
|
||||
return <PatientInfoPage />;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { ClinicalDisclaimer, FeaturedGrid, IconCards, InnerHero, PricingTiles, ServiceMatrix } from "@/widgets/template-ui";
|
||||
import { ServicesPage } from "@/widgets/services-page";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<InnerHero
|
||||
eyebrow="Clinical routes"
|
||||
title="Услуги оформлены как маршруты: цель, формат, подготовка, результат визита"
|
||||
text="Страница не обещает медицинский исход. Она объясняет, как пациент попадает к врачу, какие исследования могут быть частью маршрута и где нужен follow-up."
|
||||
/>
|
||||
<ServiceMatrix />
|
||||
<FeaturedGrid
|
||||
eyebrow="Programs"
|
||||
title="Популярные плановые маршруты"
|
||||
text="Карточки готовы под реальные направления клиники: терапия, диагностика, эндокринология и сопровождение."
|
||||
/>
|
||||
<IconCards />
|
||||
<PricingTiles title="Форматы взаимодействия" />
|
||||
<ClinicalDisclaimer />
|
||||
</>
|
||||
);
|
||||
return <ServicesPage />;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { CtaPanel, DoctorGrid, InnerHero, SpecialistSchedule } from "@/widgets/template-ui";
|
||||
import { SpecialistsPage } from "@/widgets/specialists-page";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<InnerHero
|
||||
eyebrow="Clinical team"
|
||||
title="Команда показывает не только врачей, но и роль координатора"
|
||||
text="Для плановой клиники важно объяснить, кто принимает медицинские решения, а кто помогает пройти маршрут без потери документов и follow-up."
|
||||
/>
|
||||
<DoctorGrid />
|
||||
<SpecialistSchedule />
|
||||
<CtaPanel
|
||||
title="Не знаете, с какого специалиста начать?"
|
||||
text="Оставьте плановую заявку: координатор уточнит цель визита и предложит безопасный маршрут записи без онлайн-диагноза."
|
||||
href="/booking"
|
||||
label="Описать цель визита"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
return <SpecialistsPage />;
|
||||
}
|
||||
|
||||
5
src/shared/ui/clinical-panel.tsx
Normal file
5
src/shared/ui/clinical-panel.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function ClinicalPanel({ children, className = "" }: { children: ReactNode; className?: string }) {
|
||||
return <div className={`rounded-md border border-border bg-card shadow-[0_1px_0_rgba(24,74,68,0.05)] ${className}`}>{children}</div>;
|
||||
}
|
||||
5
src/shared/ui/container.tsx
Normal file
5
src/shared/ui/container.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function Container({ children, className = "" }: { children: ReactNode; className?: string }) {
|
||||
return <section className={`mx-auto w-full max-w-[1320px] px-5 sm:px-8 ${className}`}>{children}</section>;
|
||||
}
|
||||
25
src/shared/ui/icon-cards.tsx
Normal file
25
src/shared/ui/icon-cards.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
import { carePillars } from "@/entities/site-content";
|
||||
import { ClinicalPanel } from "@/shared/ui/clinical-panel";
|
||||
import { Container } from "@/shared/ui/container";
|
||||
|
||||
type IconComponent = ComponentType<{ className?: string }>;
|
||||
type IconItem = { title: string; text: string; icon: IconComponent };
|
||||
|
||||
export function IconCards({ items = carePillars }: { items?: readonly IconItem[] }) {
|
||||
return (
|
||||
<Container className="grid gap-4 py-12 md:grid-cols-3">
|
||||
{items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<ClinicalPanel key={item.title} className="p-6">
|
||||
<Icon className="mb-8 size-6 text-primary" />
|
||||
<h3 className="text-xl font-semibold">{item.title}</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
||||
</ClinicalPanel>
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
23
src/shared/ui/info-columns.tsx
Normal file
23
src/shared/ui/info-columns.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { FileTextIcon } from "lucide-react";
|
||||
|
||||
import { ClinicalPanel } from "@/shared/ui/clinical-panel";
|
||||
import { Container } from "@/shared/ui/container";
|
||||
|
||||
type InfoItem = { title: string; text: string };
|
||||
|
||||
export function InfoColumns({ title, items }: { title: string; items: readonly InfoItem[] }) {
|
||||
return (
|
||||
<Container className="py-12">
|
||||
<h2 className="mb-6 text-4xl font-semibold">{title}</h2>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{items.map((item) => (
|
||||
<ClinicalPanel key={item.title} className="p-6">
|
||||
<FileTextIcon className="mb-8 size-5 text-primary" />
|
||||
<h3 className="text-xl font-semibold">{item.title}</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
||||
</ClinicalPanel>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
16
src/shared/ui/inner-hero.tsx
Normal file
16
src/shared/ui/inner-hero.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Container } from "@/shared/ui/container";
|
||||
|
||||
export function InnerHero({ eyebrow, title, text }: { eyebrow: string; title: string; text: string }) {
|
||||
return (
|
||||
<section className="border-b border-border bg-card">
|
||||
<Container className="py-14">
|
||||
<Badge variant="outline" className="mb-6 rounded-md border-primary/30 text-primary">
|
||||
{eyebrow}
|
||||
</Badge>
|
||||
<h1 className="max-w-[980px] text-4xl font-semibold leading-[1.04] tracking-normal sm:text-6xl">{title}</h1>
|
||||
<p className="mt-5 max-w-[780px] text-base leading-7 text-muted-foreground">{text}</p>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
19
src/shared/ui/section-header.tsx
Normal file
19
src/shared/ui/section-header.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
export function SectionHeader({
|
||||
eyebrow,
|
||||
title,
|
||||
text,
|
||||
}: {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
text?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-8 grid gap-4 md:grid-cols-[0.82fr_1fr] md:items-end">
|
||||
<div>
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-primary">{eyebrow}</div>
|
||||
<h2 className="mt-2 text-4xl font-semibold leading-tight tracking-normal sm:text-5xl">{title}</h2>
|
||||
</div>
|
||||
{text ? <p className="max-w-[660px] text-sm leading-7 text-muted-foreground sm:text-base">{text}</p> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
src/shared/ui/split-story.tsx
Normal file
40
src/shared/ui/split-story.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import Image from "next/image";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
|
||||
import { ClinicalPanel } from "@/shared/ui/clinical-panel";
|
||||
import { Container } from "@/shared/ui/container";
|
||||
|
||||
export function SplitStory({
|
||||
image,
|
||||
eyebrow,
|
||||
title,
|
||||
text,
|
||||
points,
|
||||
}: {
|
||||
image: string;
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
text: string;
|
||||
points: readonly string[];
|
||||
}) {
|
||||
return (
|
||||
<Container className="grid gap-10 py-14 lg:grid-cols-2">
|
||||
<ClinicalPanel className="relative min-h-[470px] overflow-hidden bg-muted">
|
||||
<Image src={image} alt={title} fill className="object-cover" sizes="(min-width: 1024px) 50vw, 100vw" />
|
||||
</ClinicalPanel>
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-primary">{eyebrow}</div>
|
||||
<h2 className="text-4xl font-semibold leading-tight sm:text-5xl">{title}</h2>
|
||||
<p className="mt-5 text-base leading-7 text-muted-foreground">{text}</p>
|
||||
<div className="mt-7 grid gap-3">
|
||||
{points.map((point) => (
|
||||
<div key={point} className="flex items-center gap-3 text-sm font-semibold">
|
||||
<CheckIcon className="size-4 text-primary" />
|
||||
{point}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
77
src/widgets/booking-page.tsx
Normal file
77
src/widgets/booking-page.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
|
||||
import { CalendarCheckIcon } from "lucide-react";
|
||||
|
||||
import { bookingReasons } from "@/entities/site-content";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Container } from "@/shared/ui/container";
|
||||
import { InnerHero } from "@/shared/ui/inner-hero";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Textarea } from "@/shared/ui/textarea";
|
||||
import { ClinicalDisclaimer } from "@/widgets/clinical-disclaimer";
|
||||
import { PatientChecklist } from "@/widgets/patient-checklist";
|
||||
import { PatientJourney } from "@/widgets/patient-journey";
|
||||
import { SafetyNotes } from "@/widgets/safety-notes";
|
||||
import { SpecialistSchedule } from "@/widgets/specialist-schedule";
|
||||
|
||||
function ReservationForm() {
|
||||
return (
|
||||
<Container className="grid gap-8 py-14 lg:grid-cols-[0.82fr_1.18fr]">
|
||||
<div>
|
||||
<Badge variant="outline" className="mb-5 rounded-md border-primary/30 text-primary">
|
||||
<CalendarCheckIcon className="size-4" />
|
||||
Triage form
|
||||
</Badge>
|
||||
<h2 className="text-4xl font-semibold leading-tight sm:text-5xl">Запись начинается с цели визита</h2>
|
||||
<p className="mt-4 text-sm leading-7 text-muted-foreground">
|
||||
Форма статическая. Она показывает будущий booking flow: причина обращения, желаемый маршрут, документы и безопасное предупреждение
|
||||
о неэкстренном формате.
|
||||
</p>
|
||||
<div className="mt-6 rounded-md border border-amber-200 bg-amber-50 p-4 text-sm leading-6 text-amber-900">
|
||||
При резком ухудшении состояния, сильной боли, травме или угрозе жизни нужно обращаться в экстренные службы.
|
||||
</div>
|
||||
</div>
|
||||
<form className="grid gap-4 rounded-md border border-border bg-card p-5">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<Input placeholder="Имя пациента" />
|
||||
<Input placeholder="Телефон или email" />
|
||||
<Input placeholder="Желаемая дата" />
|
||||
<Input placeholder="Удобное время" />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="text-sm font-semibold">Причина обращения</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{bookingReasons.map((reason) => (
|
||||
<button key={reason} type="button" className="rounded-md border border-border bg-background px-3 py-2 text-sm font-medium text-muted-foreground">
|
||||
{reason}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Textarea placeholder="Кратко опишите цель визита, прошлые исследования, ограничения или вопросы координатору" />
|
||||
<Button type="button" className="h-11 rounded-md">
|
||||
Отправить заявку координатору
|
||||
</Button>
|
||||
</form>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function BookingPage() {
|
||||
return (
|
||||
<>
|
||||
<InnerHero
|
||||
eyebrow="Booking"
|
||||
title="Запись как triage: цель визита, подготовка и неэкстренный контекст"
|
||||
text="Форма не отправляет данные наружу. Она показывает UI будущей записи: причина обращения, слот, документы и предупреждение о плановом формате."
|
||||
/>
|
||||
<SpecialistSchedule />
|
||||
<ReservationForm />
|
||||
<PatientJourney />
|
||||
<PatientChecklist />
|
||||
<SafetyNotes />
|
||||
<ClinicalDisclaimer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
19
src/widgets/clinical-disclaimer.tsx
Normal file
19
src/widgets/clinical-disclaimer.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { StethoscopeIcon } from "lucide-react";
|
||||
|
||||
import { Container } from "@/shared/ui/container";
|
||||
|
||||
export function ClinicalDisclaimer() {
|
||||
return (
|
||||
<Container className="py-10">
|
||||
<div className="rounded-md border border-amber-200 bg-amber-50 p-5 text-sm leading-7 text-amber-950">
|
||||
<div className="flex items-start gap-3">
|
||||
<StethoscopeIcon className="mt-1 size-5 shrink-0" />
|
||||
<p>
|
||||
Northline Clinic в этом шаблоне описывает плановый пациентский путь. Тексты не являются медицинской рекомендацией,
|
||||
не ставят диагноз и не заменяют консультацию врача.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
58
src/widgets/contacts-page.tsx
Normal file
58
src/widgets/contacts-page.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { ClockIcon } from "lucide-react";
|
||||
|
||||
import { contactCards, contactChannels } from "@/entities/site-content";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { ClinicalPanel } from "@/shared/ui/clinical-panel";
|
||||
import { Container } from "@/shared/ui/container";
|
||||
import { IconCards } from "@/shared/ui/icon-cards";
|
||||
import { InnerHero } from "@/shared/ui/inner-hero";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Textarea } from "@/shared/ui/textarea";
|
||||
import { FacilityBoard } from "@/widgets/facility-board";
|
||||
|
||||
function ContactForm() {
|
||||
return (
|
||||
<Container className="grid gap-6 py-14 lg:grid-cols-[0.95fr_1.05fr]">
|
||||
<ClinicalPanel className="p-6">
|
||||
<ClockIcon className="mb-8 size-6 text-primary" />
|
||||
<h2 className="text-4xl font-semibold">Связаться с координатором</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-muted-foreground">
|
||||
Контактная страница должна отвечать на практичные вопросы: как добраться, когда работает лабораторное окно и что взять на прием.
|
||||
</p>
|
||||
<div className="mt-6 grid gap-3">
|
||||
{contactChannels.map((channel) => (
|
||||
<div key={channel.label} className="flex items-center justify-between gap-4 rounded-md bg-muted px-4 py-3 text-sm">
|
||||
<span className="font-semibold">{channel.label}</span>
|
||||
<span className="text-muted-foreground">{channel.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ClinicalPanel>
|
||||
<form className="grid gap-3 rounded-md border border-border bg-card p-5">
|
||||
<Input placeholder="Ваше имя" />
|
||||
<Input placeholder="Email или телефон" />
|
||||
<Textarea placeholder="Вопрос координатору, документы, страховая или желаемый маршрут" />
|
||||
<Button type="button" className="h-11 rounded-md">
|
||||
Отправить сообщение
|
||||
</Button>
|
||||
</form>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function ContactsPage() {
|
||||
return (
|
||||
<>
|
||||
<InnerHero
|
||||
eyebrow="Contacts"
|
||||
title="Адрес, часы, лабораторное окно и связь с координатором"
|
||||
text="Контактная страница клиники должна помогать пациенту приехать подготовленным: адрес, время, документы, способ связи и понятное ожидание ответа."
|
||||
/>
|
||||
<IconCards items={contactCards} />
|
||||
<FacilityBoard />
|
||||
<ContactForm />
|
||||
</>
|
||||
);
|
||||
}
|
||||
35
src/widgets/facility-board.tsx
Normal file
35
src/widgets/facility-board.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import { facilityCards, site } from "@/entities/site-content";
|
||||
import { ClinicalPanel } from "@/shared/ui/clinical-panel";
|
||||
import { Container } from "@/shared/ui/container";
|
||||
|
||||
export function FacilityBoard() {
|
||||
return (
|
||||
<Container className="py-12">
|
||||
<div className="grid gap-4 lg:grid-cols-[1fr_1.05fr]">
|
||||
<ClinicalPanel className="relative min-h-[440px] overflow-hidden bg-muted">
|
||||
<Image
|
||||
src={site.accentImage}
|
||||
alt="Клиническая зона диагностики"
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(min-width: 1024px) 50vw, 100vw"
|
||||
/>
|
||||
</ClinicalPanel>
|
||||
<div className="grid gap-4">
|
||||
{facilityCards.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<ClinicalPanel key={item.title} className="p-6">
|
||||
<Icon className="mb-7 size-6 text-primary" />
|
||||
<h3 className="text-2xl font-semibold">{item.title}</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
||||
</ClinicalPanel>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
67
src/widgets/featured-grid.tsx
Normal file
67
src/widgets/featured-grid.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { serviceLines } from "@/entities/site-content";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { ClinicalPanel } from "@/shared/ui/clinical-panel";
|
||||
import { Container } from "@/shared/ui/container";
|
||||
import { SectionHeader } from "@/shared/ui/section-header";
|
||||
import { Separator } from "@/shared/ui/separator";
|
||||
|
||||
type ServiceItem = {
|
||||
name: string;
|
||||
department: string;
|
||||
duration: string;
|
||||
price: string;
|
||||
tag: string;
|
||||
text: string;
|
||||
includes: readonly string[];
|
||||
preparation: string;
|
||||
};
|
||||
|
||||
export function FeaturedGrid({
|
||||
eyebrow,
|
||||
title,
|
||||
text,
|
||||
items = serviceLines,
|
||||
}: {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
text: string;
|
||||
items?: readonly ServiceItem[];
|
||||
}) {
|
||||
return (
|
||||
<Container className="py-14">
|
||||
<SectionHeader eyebrow={eyebrow} title={title} text={text} />
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{items.map((item) => (
|
||||
<ClinicalPanel key={item.name} className="p-5">
|
||||
<div className="mb-5 flex flex-wrap items-center gap-2">
|
||||
<Badge variant="secondary" className="rounded-md">{item.tag}</Badge>
|
||||
<Badge variant="outline" className="rounded-md">{item.department}</Badge>
|
||||
<span className="text-sm font-semibold text-primary">{item.duration}</span>
|
||||
</div>
|
||||
<div className="grid gap-5 sm:grid-cols-[1fr_150px]">
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold">{item.name}</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
||||
</div>
|
||||
<div className="text-left sm:text-right">
|
||||
<div className="text-2xl font-semibold">{item.price}</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">ориентир, не оферта</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="my-5" />
|
||||
<div className="grid gap-4 md:grid-cols-[1fr_1.2fr]">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{item.includes.map((line) => (
|
||||
<span key={line} className="rounded-md bg-muted px-3 py-2 text-xs font-semibold text-muted-foreground">
|
||||
{line}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm leading-6 text-muted-foreground">{item.preparation}</p>
|
||||
</div>
|
||||
</ClinicalPanel>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
124
src/widgets/home-page.tsx
Normal file
124
src/widgets/home-page.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { ArrowRightIcon, CalendarCheckIcon } from "lucide-react";
|
||||
|
||||
import { highlights, routeBadges, site, testimonials } from "@/entities/site-content";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { ClinicalPanel } from "@/shared/ui/clinical-panel";
|
||||
import { Container } from "@/shared/ui/container";
|
||||
import { ClinicalDisclaimer } from "@/widgets/clinical-disclaimer";
|
||||
import { FacilityBoard } from "@/widgets/facility-board";
|
||||
import { FeaturedGrid } from "@/widgets/featured-grid";
|
||||
import { PatientChecklist } from "@/widgets/patient-checklist";
|
||||
import { PatientJourney } from "@/widgets/patient-journey";
|
||||
import { SafetyNotes } from "@/widgets/safety-notes";
|
||||
import { SpecialistSchedule } from "@/widgets/specialist-schedule";
|
||||
|
||||
function PageHero() {
|
||||
return (
|
||||
<section className="relative overflow-hidden border-b border-border">
|
||||
<div className="clinic-soft-grid absolute inset-0" />
|
||||
<Container className="relative grid min-h-[760px] items-center gap-10 py-14 lg:grid-cols-[0.88fr_1.12fr]">
|
||||
<div>
|
||||
<Badge variant="outline" className="mb-6 w-fit rounded-md border-primary/30 bg-background/75 text-primary">
|
||||
Плановый медицинский маршрут
|
||||
</Badge>
|
||||
<h1 className="max-w-[740px] text-4xl font-semibold leading-[1.02] tracking-normal sm:text-6xl lg:text-[4.9rem]">
|
||||
Клиника, где пациент понимает путь до первого визита
|
||||
</h1>
|
||||
<p className="mt-6 max-w-[620px] text-lg leading-8 text-muted-foreground">{site.tagline}</p>
|
||||
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
||||
<Button asChild className="h-12 rounded-md px-6">
|
||||
<Link href="/booking">
|
||||
{site.cta}
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" className="h-12 rounded-md bg-background/60 px-6">
|
||||
<Link href="/services">{site.secondaryCta}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-8 flex flex-wrap gap-2">
|
||||
{routeBadges.map((badge) => (
|
||||
<span key={badge} className="rounded-md border border-border bg-card/86 px-3 py-2 text-xs font-semibold text-muted-foreground">
|
||||
{badge}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 xl:grid-cols-[1fr_280px]">
|
||||
<ClinicalPanel className="relative min-h-[590px] overflow-hidden bg-muted">
|
||||
<Image
|
||||
src={site.heroImage}
|
||||
alt="Врач спокойно разговаривает с пациентом"
|
||||
fill
|
||||
priority
|
||||
className="object-cover"
|
||||
sizes="(min-width: 1280px) 650px, 100vw"
|
||||
/>
|
||||
<div className="absolute bottom-5 left-5 right-5 rounded-md bg-background/94 p-5 shadow-xl backdrop-blur">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-primary">следующий слот</div>
|
||||
<div className="mt-2 text-xl font-semibold">Первичная навигация сегодня 16:30</div>
|
||||
<p className="mt-1 text-sm text-muted-foreground">Координатор подтвердит цель визита и документы.</p>
|
||||
</div>
|
||||
<CalendarCheckIcon className="size-5 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</ClinicalPanel>
|
||||
<div className="grid content-center gap-3">
|
||||
{highlights.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<ClinicalPanel key={item.title} className="p-4">
|
||||
<Icon className="mb-6 size-5 text-primary" />
|
||||
<div className="text-2xl font-semibold text-primary">{item.value}</div>
|
||||
<h3 className="mt-2 font-semibold">{item.title}</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
||||
</ClinicalPanel>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function TestimonialBand({ items = testimonials }: { items?: readonly { name: string; text: string; rating: string }[] }) {
|
||||
return (
|
||||
<Container className="grid gap-4 py-12 md:grid-cols-2">
|
||||
{items.map((item) => (
|
||||
<blockquote key={item.name} className="rounded-md border border-border bg-card p-7">
|
||||
<div className="text-sm font-semibold text-primary">{item.rating}</div>
|
||||
<p className="mt-5 text-2xl font-semibold leading-snug">"{item.text}"</p>
|
||||
<footer className="mt-5 text-sm text-muted-foreground">{item.name}</footer>
|
||||
</blockquote>
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<PageHero />
|
||||
<PatientJourney />
|
||||
<FeaturedGrid
|
||||
eyebrow="Care programs"
|
||||
title="Маршруты, которые продают порядок, а не обещания"
|
||||
text="Каждая услуга показывает формат, подготовку, состав маршрута и безопасный next step для пациента."
|
||||
/>
|
||||
<FacilityBoard />
|
||||
<SpecialistSchedule />
|
||||
<PatientChecklist />
|
||||
<SafetyNotes />
|
||||
<TestimonialBand />
|
||||
<ClinicalDisclaimer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
26
src/widgets/metric-strip.tsx
Normal file
26
src/widgets/metric-strip.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
import { highlights } from "@/entities/site-content";
|
||||
import { ClinicalPanel } from "@/shared/ui/clinical-panel";
|
||||
import { Container } from "@/shared/ui/container";
|
||||
|
||||
type IconComponent = ComponentType<{ className?: string }>;
|
||||
type MetricItem = { title: string; value: string; text: string; icon: IconComponent };
|
||||
|
||||
export function MetricStrip({ items = highlights }: { items?: readonly MetricItem[] }) {
|
||||
return (
|
||||
<Container className="grid gap-3 py-8 md:grid-cols-3">
|
||||
{items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<ClinicalPanel key={item.title} className="p-5">
|
||||
<Icon className="mb-6 size-5 text-primary" />
|
||||
<div className="text-4xl font-semibold">{item.value}</div>
|
||||
<h3 className="mt-2 font-semibold">{item.title}</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
||||
</ClinicalPanel>
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
28
src/widgets/patient-checklist.tsx
Normal file
28
src/widgets/patient-checklist.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { patientChecklist } from "@/entities/site-content";
|
||||
import { ClinicalPanel } from "@/shared/ui/clinical-panel";
|
||||
import { Container } from "@/shared/ui/container";
|
||||
import { SectionHeader } from "@/shared/ui/section-header";
|
||||
|
||||
export function PatientChecklist() {
|
||||
return (
|
||||
<Container className="py-14">
|
||||
<SectionHeader
|
||||
eyebrow="Before visit"
|
||||
title="Пациентская подготовка без скрытых ожиданий"
|
||||
text="Эта секция закрывает один из главных вопросов медицинского сайта: что взять с собой и почему это нужно."
|
||||
/>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{patientChecklist.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<ClinicalPanel key={item.title} className="p-6">
|
||||
<Icon className="mb-8 size-6 text-primary" />
|
||||
<h3 className="text-xl font-semibold">{item.title}</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
||||
</ClinicalPanel>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
49
src/widgets/patient-info-page.tsx
Normal file
49
src/widgets/patient-info-page.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { ShieldCheckIcon } from "lucide-react";
|
||||
|
||||
import { insuranceNotes } from "@/entities/site-content";
|
||||
import { ClinicalPanel } from "@/shared/ui/clinical-panel";
|
||||
import { Container } from "@/shared/ui/container";
|
||||
import { InnerHero } from "@/shared/ui/inner-hero";
|
||||
import { SectionHeader } from "@/shared/ui/section-header";
|
||||
import { ClinicalDisclaimer } from "@/widgets/clinical-disclaimer";
|
||||
import { PatientChecklist } from "@/widgets/patient-checklist";
|
||||
import { SafetyNotes } from "@/widgets/safety-notes";
|
||||
|
||||
function InsurancePanel() {
|
||||
return (
|
||||
<Container className="py-12">
|
||||
<SectionHeader
|
||||
eyebrow="Documents and privacy"
|
||||
title="Страховка, заключения и конфиденциальность вынесены в явный сервисный слой"
|
||||
text="Для клиники важно показать не только врачей, но и то, как пациент получает документы, где хранятся результаты и что происходит с оплатой."
|
||||
/>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{insuranceNotes.map((item) => (
|
||||
<ClinicalPanel key={item.title} className="p-6">
|
||||
<ShieldCheckIcon className="mb-8 size-6 text-primary" />
|
||||
<h3 className="text-xl font-semibold">{item.title}</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
||||
</ClinicalPanel>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function PatientInfoPage() {
|
||||
return (
|
||||
<>
|
||||
<InnerHero
|
||||
eyebrow="Patient info"
|
||||
title="Документы, страховые вопросы, результаты и правила планового визита"
|
||||
text="Сервисная страница закрывает вопросы до записи: что взять, как передаются заключения, почему форма не заменяет консультацию и где проходит граница экстренной помощи."
|
||||
/>
|
||||
<PatientChecklist />
|
||||
<InsurancePanel />
|
||||
<SafetyNotes />
|
||||
<ClinicalDisclaimer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
27
src/widgets/patient-journey.tsx
Normal file
27
src/widgets/patient-journey.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { carePathway } from "@/entities/site-content";
|
||||
import { ClinicalPanel } from "@/shared/ui/clinical-panel";
|
||||
import { Container } from "@/shared/ui/container";
|
||||
import { SectionHeader } from "@/shared/ui/section-header";
|
||||
|
||||
export function PatientJourney() {
|
||||
return (
|
||||
<Container className="py-14">
|
||||
<SectionHeader
|
||||
eyebrow="Patient journey"
|
||||
title="Маршрут пациента как главный продукт клиники"
|
||||
text="Клинический сайт должен снижать неопределенность: зачем визит, как подготовиться, кто отвечает и что будет после приема."
|
||||
/>
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
{carePathway.map((step) => (
|
||||
<ClinicalPanel key={step.step} className="p-5">
|
||||
<div className="mb-8 flex size-11 items-center justify-center rounded-md bg-primary text-sm font-semibold text-primary-foreground">
|
||||
{step.step}
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">{step.title}</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{step.text}</p>
|
||||
</ClinicalPanel>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
20
src/widgets/safety-notes.tsx
Normal file
20
src/widgets/safety-notes.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { safetyNotes } from "@/entities/site-content";
|
||||
import { ClinicalPanel } from "@/shared/ui/clinical-panel";
|
||||
import { Container } from "@/shared/ui/container";
|
||||
|
||||
export function SafetyNotes() {
|
||||
return (
|
||||
<Container className="grid gap-4 py-12 md:grid-cols-3">
|
||||
{safetyNotes.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<ClinicalPanel key={item.title} className="p-6">
|
||||
<Icon className="mb-8 size-6 text-primary" />
|
||||
<h3 className="text-xl font-semibold">{item.title}</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
||||
</ClinicalPanel>
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
86
src/widgets/services-page.tsx
Normal file
86
src/widgets/services-page.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import { CheckIcon } from "lucide-react";
|
||||
|
||||
import { serviceLines, visitFormats } from "@/entities/site-content";
|
||||
import { ClinicalPanel } from "@/shared/ui/clinical-panel";
|
||||
import { Container } from "@/shared/ui/container";
|
||||
import { IconCards } from "@/shared/ui/icon-cards";
|
||||
import { InnerHero } from "@/shared/ui/inner-hero";
|
||||
import { Separator } from "@/shared/ui/separator";
|
||||
import { ClinicalDisclaimer } from "@/widgets/clinical-disclaimer";
|
||||
import { FeaturedGrid } from "@/widgets/featured-grid";
|
||||
|
||||
type TileItem = { title: string; price: string; items: readonly string[] };
|
||||
|
||||
function ServiceMatrix() {
|
||||
return (
|
||||
<Container className="py-14">
|
||||
<ClinicalPanel className="overflow-hidden">
|
||||
<div className="grid border-b border-border bg-muted px-4 py-3 text-xs font-semibold uppercase tracking-[0.12em] text-muted-foreground md:grid-cols-[1.1fr_0.9fr_120px_120px]">
|
||||
<div>Маршрут</div>
|
||||
<div>Что закрывает</div>
|
||||
<div>Формат</div>
|
||||
<div className="md:text-right">Ориентир</div>
|
||||
</div>
|
||||
{serviceLines.map((item) => (
|
||||
<div key={item.name} className="grid gap-3 border-b border-border px-4 py-4 last:border-b-0 md:grid-cols-[1.1fr_0.9fr_120px_120px] md:items-center">
|
||||
<div>
|
||||
<div className="font-semibold">{item.name}</div>
|
||||
<div className="mt-1 text-sm text-muted-foreground">{item.department}</div>
|
||||
</div>
|
||||
<div className="text-sm leading-6 text-muted-foreground">{item.text}</div>
|
||||
<div className="text-sm font-semibold text-primary">{item.duration}</div>
|
||||
<div className="font-semibold md:text-right">{item.price}</div>
|
||||
</div>
|
||||
))}
|
||||
</ClinicalPanel>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function PricingTiles({ title, items = visitFormats }: { title: string; items?: readonly TileItem[] }) {
|
||||
return (
|
||||
<Container className="py-14">
|
||||
<h2 className="mb-7 text-4xl font-semibold sm:text-5xl">{title}</h2>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{items.map((item) => (
|
||||
<ClinicalPanel key={item.title} className="p-6">
|
||||
<h3 className="text-2xl font-semibold">{item.title}</h3>
|
||||
<div className="mt-3 text-3xl font-semibold text-primary">{item.price}</div>
|
||||
<Separator className="my-5" />
|
||||
<div className="grid gap-2">
|
||||
{item.items.map((line) => (
|
||||
<div key={line} className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<CheckIcon className="size-4 text-primary" />
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ClinicalPanel>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function ServicesPage() {
|
||||
return (
|
||||
<>
|
||||
<InnerHero
|
||||
eyebrow="Clinical routes"
|
||||
title="Услуги оформлены как маршруты: цель, формат, подготовка, результат визита"
|
||||
text="Страница не обещает медицинский исход. Она объясняет, как пациент попадает к врачу, какие исследования могут быть частью маршрута и где нужен follow-up."
|
||||
/>
|
||||
<ServiceMatrix />
|
||||
<FeaturedGrid
|
||||
eyebrow="Programs"
|
||||
title="Популярные плановые маршруты"
|
||||
text="Карточки готовы под реальные направления клиники: терапия, диагностика, эндокринология и сопровождение."
|
||||
/>
|
||||
<IconCards />
|
||||
<PricingTiles title="Форматы взаимодействия" />
|
||||
<ClinicalDisclaimer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
74
src/widgets/site-shell.tsx
Normal file
74
src/widgets/site-shell.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import Link from "next/link";
|
||||
import { MenuIcon, ShieldCheckIcon } from "lucide-react";
|
||||
|
||||
import { site } from "@/entities/site-content";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
|
||||
export function SiteHeader() {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 border-b border-border bg-background/94 backdrop-blur-xl">
|
||||
<div className="mx-auto flex h-[72px] w-full max-w-[1320px] items-center justify-between px-5 sm:px-8">
|
||||
<Link href="/" className="flex items-center gap-3">
|
||||
<span className="flex size-10 items-center justify-center rounded-md bg-primary text-sm font-bold text-primary-foreground">
|
||||
NL
|
||||
</span>
|
||||
<span>
|
||||
<span className="block text-xl font-semibold leading-none">{site.name}</span>
|
||||
<span className="hidden text-[11px] uppercase tracking-[0.18em] text-muted-foreground sm:block">
|
||||
planned care clinic
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
<nav className="hidden items-center gap-1 lg:flex">
|
||||
{site.nav.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="rounded-md px-3 py-2 text-sm font-semibold text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="hidden rounded-md border-primary/30 bg-card text-primary md:inline-flex">
|
||||
<ShieldCheckIcon className="size-3.5" />
|
||||
неэкстренный прием
|
||||
</Badge>
|
||||
<Button asChild className="hidden h-11 rounded-md px-5 sm:inline-flex">
|
||||
<Link href="/booking">{site.cta}</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" className="rounded-md lg:hidden" aria-label="Открыть меню">
|
||||
<MenuIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export function SiteFooter() {
|
||||
return (
|
||||
<footer className="border-t border-border bg-card">
|
||||
<div className="mx-auto grid w-full max-w-[1320px] gap-8 px-5 py-10 sm:px-8 md:grid-cols-[1.35fr_1fr_1fr]">
|
||||
<div>
|
||||
<div className="text-2xl font-semibold">{site.name}</div>
|
||||
<p className="mt-4 max-w-[520px] text-sm leading-6 text-muted-foreground">{site.tagline}</p>
|
||||
</div>
|
||||
<div className="grid gap-2 text-sm text-muted-foreground">
|
||||
<div className="mb-2 font-semibold text-foreground">Пациентский путь</div>
|
||||
{site.nav.map((item) => (
|
||||
<Link key={item.href} href={item.href} className="hover:text-foreground">
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-sm leading-6 text-muted-foreground">
|
||||
<div className="mb-2 font-semibold text-foreground">Важно</div>
|
||||
Шаблон не ставит диагнозы, не принимает платежи и не отправляет медицинские данные.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
38
src/widgets/specialist-schedule.tsx
Normal file
38
src/widgets/specialist-schedule.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ClockIcon } from "lucide-react";
|
||||
|
||||
import { scheduleRows } from "@/entities/site-content";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { ClinicalPanel } from "@/shared/ui/clinical-panel";
|
||||
import { Container } from "@/shared/ui/container";
|
||||
|
||||
export function SpecialistSchedule() {
|
||||
return (
|
||||
<Container className="py-10">
|
||||
<ClinicalPanel className="p-5">
|
||||
<div className="mb-5 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-semibold">Ближайшие плановые слоты</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">Слоты показывают врача, кабинет и подготовку, а не просто время.</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="w-fit rounded-md border-primary/30 text-primary">
|
||||
<ClockIcon className="size-3.5" />
|
||||
не для острых состояний
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
{scheduleRows.map((row) => (
|
||||
<div key={`${row.service}-${row.time}`} className="grid gap-3 rounded-md border border-border bg-background p-4 lg:grid-cols-[1fr_190px_120px_180px] lg:items-center">
|
||||
<div>
|
||||
<div className="font-semibold">{row.service}</div>
|
||||
<div className="mt-1 text-sm text-muted-foreground">{row.specialist}</div>
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-primary">{row.time}</div>
|
||||
<div className="text-sm text-muted-foreground">{row.room}</div>
|
||||
<div className="text-sm text-muted-foreground">{row.prep}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ClinicalPanel>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
71
src/widgets/specialists-page.tsx
Normal file
71
src/widgets/specialists-page.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { AlertCircleIcon, ArrowRightIcon, UserRoundIcon } from "lucide-react";
|
||||
|
||||
import { doctors } from "@/entities/site-content";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { ClinicalPanel } from "@/shared/ui/clinical-panel";
|
||||
import { Container } from "@/shared/ui/container";
|
||||
import { InnerHero } from "@/shared/ui/inner-hero";
|
||||
import { SpecialistSchedule } from "@/widgets/specialist-schedule";
|
||||
|
||||
function DoctorGrid() {
|
||||
return (
|
||||
<Container className="grid gap-4 py-12 md:grid-cols-2 xl:grid-cols-4">
|
||||
{doctors.map((doctor) => (
|
||||
<ClinicalPanel key={doctor.name} className="p-6">
|
||||
<div className="mb-7 flex size-14 items-center justify-center rounded-md bg-primary/10 text-primary">
|
||||
<UserRoundIcon className="size-7" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold">{doctor.name}</h3>
|
||||
<p className="mt-2 text-sm font-semibold text-primary">{doctor.role}</p>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{doctor.focus}</p>
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
<Badge variant="outline" className="rounded-md">{doctor.experience}</Badge>
|
||||
<Badge variant="secondary" className="rounded-md">{doctor.nextSlot}</Badge>
|
||||
</div>
|
||||
</ClinicalPanel>
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function CtaPanel({ title, text, href, label }: { title: string; text: string; href: string; label: string }) {
|
||||
return (
|
||||
<Container className="py-12">
|
||||
<div className="rounded-md bg-primary p-8 text-primary-foreground">
|
||||
<AlertCircleIcon className="mb-8 size-6" />
|
||||
<h2 className="text-4xl font-semibold">{title}</h2>
|
||||
<p className="mt-4 max-w-[720px] text-sm leading-7 text-primary-foreground/72">{text}</p>
|
||||
<Button asChild variant="secondary" className="mt-6 rounded-md">
|
||||
<Link href={href}>
|
||||
{label}
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function SpecialistsPage() {
|
||||
return (
|
||||
<>
|
||||
<InnerHero
|
||||
eyebrow="Clinical team"
|
||||
title="Команда показывает не только врачей, но и роль координатора"
|
||||
text="Для плановой клиники важно объяснить, кто принимает медицинские решения, а кто помогает пройти маршрут без потери документов и follow-up."
|
||||
/>
|
||||
<DoctorGrid />
|
||||
<SpecialistSchedule />
|
||||
<CtaPanel
|
||||
title="Не знаете, с какого специалиста начать?"
|
||||
text="Оставьте плановую заявку: координатор уточнит цель визита и предложит безопасный маршрут записи без онлайн-диагноза."
|
||||
href="/booking"
|
||||
label="Описать цель визита"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,743 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import type { ComponentType, ReactNode } from "react";
|
||||
import {
|
||||
AlertCircleIcon,
|
||||
ArrowRightIcon,
|
||||
CalendarCheckIcon,
|
||||
CheckIcon,
|
||||
ClockIcon,
|
||||
FileTextIcon,
|
||||
MenuIcon,
|
||||
ShieldCheckIcon,
|
||||
StethoscopeIcon,
|
||||
UserRoundIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import {
|
||||
bookingReasons,
|
||||
carePathway,
|
||||
carePillars,
|
||||
contactChannels,
|
||||
doctors,
|
||||
facilityCards,
|
||||
highlights,
|
||||
insuranceNotes,
|
||||
patientChecklist,
|
||||
routeBadges,
|
||||
safetyNotes,
|
||||
scheduleRows,
|
||||
serviceLines,
|
||||
site,
|
||||
testimonials,
|
||||
visitFormats,
|
||||
} from "@/entities/site-content";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Separator } from "@/shared/ui/separator";
|
||||
import { Textarea } from "@/shared/ui/textarea";
|
||||
|
||||
type IconComponent = ComponentType<{ className?: string }>;
|
||||
|
||||
type MetricItem = {
|
||||
title: string;
|
||||
value: string;
|
||||
text: string;
|
||||
icon: IconComponent;
|
||||
};
|
||||
|
||||
type ServiceItem = {
|
||||
name: string;
|
||||
department: string;
|
||||
duration: string;
|
||||
price: string;
|
||||
tag: string;
|
||||
text: string;
|
||||
includes: readonly string[];
|
||||
preparation: string;
|
||||
};
|
||||
|
||||
type TileItem = {
|
||||
title: string;
|
||||
price: string;
|
||||
items: readonly string[];
|
||||
};
|
||||
|
||||
type IconItem = {
|
||||
title: string;
|
||||
text: string;
|
||||
icon: IconComponent;
|
||||
};
|
||||
|
||||
type InfoItem = {
|
||||
title: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type TestimonialItem = {
|
||||
name: string;
|
||||
text: string;
|
||||
rating: string;
|
||||
};
|
||||
|
||||
function Container({ children, className = "" }: { children: ReactNode; className?: string }) {
|
||||
return <section className={`mx-auto w-full max-w-[1320px] px-5 sm:px-8 ${className}`}>{children}</section>;
|
||||
}
|
||||
|
||||
function SectionHeader({
|
||||
eyebrow,
|
||||
title,
|
||||
text,
|
||||
}: {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
text?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-8 grid gap-4 md:grid-cols-[0.82fr_1fr] md:items-end">
|
||||
<div>
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-primary">{eyebrow}</div>
|
||||
<h2 className="mt-2 text-4xl font-semibold leading-tight tracking-normal sm:text-5xl">{title}</h2>
|
||||
</div>
|
||||
{text ? <p className="max-w-[660px] text-sm leading-7 text-muted-foreground sm:text-base">{text}</p> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ClinicalPanel({ children, className = "" }: { children: ReactNode; className?: string }) {
|
||||
return <div className={`rounded-md border border-border bg-card shadow-[0_1px_0_rgba(24,74,68,0.05)] ${className}`}>{children}</div>;
|
||||
}
|
||||
|
||||
export function SiteHeader() {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 border-b border-border bg-background/94 backdrop-blur-xl">
|
||||
<div className="mx-auto flex h-[72px] w-full max-w-[1320px] items-center justify-between px-5 sm:px-8">
|
||||
<Link href="/" className="flex items-center gap-3">
|
||||
<span className="flex size-10 items-center justify-center rounded-md bg-primary text-sm font-bold text-primary-foreground">
|
||||
NL
|
||||
</span>
|
||||
<span>
|
||||
<span className="block text-xl font-semibold leading-none">{site.name}</span>
|
||||
<span className="hidden text-[11px] uppercase tracking-[0.18em] text-muted-foreground sm:block">
|
||||
planned care clinic
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
<nav className="hidden items-center gap-1 lg:flex">
|
||||
{site.nav.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="rounded-md px-3 py-2 text-sm font-semibold text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="hidden rounded-md border-primary/30 bg-card text-primary md:inline-flex">
|
||||
<ShieldCheckIcon className="size-3.5" />
|
||||
неэкстренный прием
|
||||
</Badge>
|
||||
<Button asChild className="hidden h-11 rounded-md px-5 sm:inline-flex">
|
||||
<Link href="/booking">{site.cta}</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" className="rounded-md lg:hidden" aria-label="Открыть меню">
|
||||
<MenuIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export function SiteFooter() {
|
||||
return (
|
||||
<footer className="border-t border-border bg-card">
|
||||
<div className="mx-auto grid w-full max-w-[1320px] gap-8 px-5 py-10 sm:px-8 md:grid-cols-[1.35fr_1fr_1fr]">
|
||||
<div>
|
||||
<div className="text-2xl font-semibold">{site.name}</div>
|
||||
<p className="mt-4 max-w-[520px] text-sm leading-6 text-muted-foreground">{site.tagline}</p>
|
||||
</div>
|
||||
<div className="grid gap-2 text-sm text-muted-foreground">
|
||||
<div className="mb-2 font-semibold text-foreground">Пациентский путь</div>
|
||||
{site.nav.map((item) => (
|
||||
<Link key={item.href} href={item.href} className="hover:text-foreground">
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-sm leading-6 text-muted-foreground">
|
||||
<div className="mb-2 font-semibold text-foreground">Важно</div>
|
||||
Шаблон не ставит диагнозы, не принимает платежи и не отправляет медицинские данные.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
export function PageHero() {
|
||||
return (
|
||||
<section className="relative overflow-hidden border-b border-border">
|
||||
<div className="clinic-soft-grid absolute inset-0" />
|
||||
<Container className="relative grid min-h-[760px] items-center gap-10 py-14 lg:grid-cols-[0.88fr_1.12fr]">
|
||||
<div>
|
||||
<Badge variant="outline" className="mb-6 w-fit rounded-md border-primary/30 bg-background/75 text-primary">
|
||||
Плановый медицинский маршрут
|
||||
</Badge>
|
||||
<h1 className="max-w-[740px] text-4xl font-semibold leading-[1.02] tracking-normal sm:text-6xl lg:text-[4.9rem]">
|
||||
Клиника, где пациент понимает путь до первого визита
|
||||
</h1>
|
||||
<p className="mt-6 max-w-[620px] text-lg leading-8 text-muted-foreground">{site.tagline}</p>
|
||||
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
||||
<Button asChild className="h-12 rounded-md px-6">
|
||||
<Link href="/booking">
|
||||
{site.cta}
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" className="h-12 rounded-md bg-background/60 px-6">
|
||||
<Link href="/services">{site.secondaryCta}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-8 flex flex-wrap gap-2">
|
||||
{routeBadges.map((badge) => (
|
||||
<span key={badge} className="rounded-md border border-border bg-card/86 px-3 py-2 text-xs font-semibold text-muted-foreground">
|
||||
{badge}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 xl:grid-cols-[1fr_280px]">
|
||||
<ClinicalPanel className="relative min-h-[590px] overflow-hidden bg-muted">
|
||||
<Image
|
||||
src={site.heroImage}
|
||||
alt="Врач спокойно разговаривает с пациентом"
|
||||
fill
|
||||
priority
|
||||
className="object-cover"
|
||||
sizes="(min-width: 1280px) 650px, 100vw"
|
||||
/>
|
||||
<div className="absolute bottom-5 left-5 right-5 rounded-md bg-background/94 p-5 shadow-xl backdrop-blur">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-primary">следующий слот</div>
|
||||
<div className="mt-2 text-xl font-semibold">Первичная навигация сегодня 16:30</div>
|
||||
<p className="mt-1 text-sm text-muted-foreground">Координатор подтвердит цель визита и документы.</p>
|
||||
</div>
|
||||
<CalendarCheckIcon className="size-5 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</ClinicalPanel>
|
||||
<div className="grid content-center gap-3">
|
||||
{highlights.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<ClinicalPanel key={item.title} className="p-4">
|
||||
<Icon className="mb-6 size-5 text-primary" />
|
||||
<div className="text-2xl font-semibold text-primary">{item.value}</div>
|
||||
<h3 className="mt-2 font-semibold">{item.title}</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
||||
</ClinicalPanel>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function InnerHero({ eyebrow, title, text }: { eyebrow: string; title: string; text: string }) {
|
||||
return (
|
||||
<section className="border-b border-border bg-card">
|
||||
<Container className="py-14">
|
||||
<Badge variant="outline" className="mb-6 rounded-md border-primary/30 text-primary">
|
||||
{eyebrow}
|
||||
</Badge>
|
||||
<h1 className="max-w-[980px] text-4xl font-semibold leading-[1.04] tracking-normal sm:text-6xl">{title}</h1>
|
||||
<p className="mt-5 max-w-[780px] text-base leading-7 text-muted-foreground">{text}</p>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function PatientJourney() {
|
||||
return (
|
||||
<Container className="py-14">
|
||||
<SectionHeader
|
||||
eyebrow="Patient journey"
|
||||
title="Маршрут пациента как главный продукт клиники"
|
||||
text="Клинический сайт должен снижать неопределенность: зачем визит, как подготовиться, кто отвечает и что будет после приема."
|
||||
/>
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
{carePathway.map((step) => (
|
||||
<ClinicalPanel key={step.step} className="p-5">
|
||||
<div className="mb-8 flex size-11 items-center justify-center rounded-md bg-primary text-sm font-semibold text-primary-foreground">
|
||||
{step.step}
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">{step.title}</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{step.text}</p>
|
||||
</ClinicalPanel>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function MetricStrip({ items = highlights }: { items?: readonly MetricItem[] }) {
|
||||
return (
|
||||
<Container className="grid gap-3 py-8 md:grid-cols-3">
|
||||
{items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<ClinicalPanel key={item.title} className="p-5">
|
||||
<Icon className="mb-6 size-5 text-primary" />
|
||||
<div className="text-4xl font-semibold">{item.value}</div>
|
||||
<h3 className="mt-2 font-semibold">{item.title}</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
||||
</ClinicalPanel>
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function FeaturedGrid({
|
||||
eyebrow,
|
||||
title,
|
||||
text,
|
||||
items = serviceLines,
|
||||
}: {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
text: string;
|
||||
items?: readonly ServiceItem[];
|
||||
}) {
|
||||
return (
|
||||
<Container className="py-14">
|
||||
<SectionHeader eyebrow={eyebrow} title={title} text={text} />
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{items.map((item) => (
|
||||
<ClinicalPanel key={item.name} className="p-5">
|
||||
<div className="mb-5 flex flex-wrap items-center gap-2">
|
||||
<Badge variant="secondary" className="rounded-md">{item.tag}</Badge>
|
||||
<Badge variant="outline" className="rounded-md">{item.department}</Badge>
|
||||
<span className="text-sm font-semibold text-primary">{item.duration}</span>
|
||||
</div>
|
||||
<div className="grid gap-5 sm:grid-cols-[1fr_150px]">
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold">{item.name}</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
||||
</div>
|
||||
<div className="text-left sm:text-right">
|
||||
<div className="text-2xl font-semibold">{item.price}</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">ориентир, не оферта</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="my-5" />
|
||||
<div className="grid gap-4 md:grid-cols-[1fr_1.2fr]">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{item.includes.map((line) => (
|
||||
<span key={line} className="rounded-md bg-muted px-3 py-2 text-xs font-semibold text-muted-foreground">
|
||||
{line}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm leading-6 text-muted-foreground">{item.preparation}</p>
|
||||
</div>
|
||||
</ClinicalPanel>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function ServiceMatrix() {
|
||||
return (
|
||||
<Container className="py-14">
|
||||
<ClinicalPanel className="overflow-hidden">
|
||||
<div className="grid border-b border-border bg-muted px-4 py-3 text-xs font-semibold uppercase tracking-[0.12em] text-muted-foreground md:grid-cols-[1.1fr_0.9fr_120px_120px]">
|
||||
<div>Маршрут</div>
|
||||
<div>Что закрывает</div>
|
||||
<div>Формат</div>
|
||||
<div className="md:text-right">Ориентир</div>
|
||||
</div>
|
||||
{serviceLines.map((item) => (
|
||||
<div key={item.name} className="grid gap-3 border-b border-border px-4 py-4 last:border-b-0 md:grid-cols-[1.1fr_0.9fr_120px_120px] md:items-center">
|
||||
<div>
|
||||
<div className="font-semibold">{item.name}</div>
|
||||
<div className="mt-1 text-sm text-muted-foreground">{item.department}</div>
|
||||
</div>
|
||||
<div className="text-sm leading-6 text-muted-foreground">{item.text}</div>
|
||||
<div className="text-sm font-semibold text-primary">{item.duration}</div>
|
||||
<div className="font-semibold md:text-right">{item.price}</div>
|
||||
</div>
|
||||
))}
|
||||
</ClinicalPanel>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function SpecialistSchedule() {
|
||||
return (
|
||||
<Container className="py-10">
|
||||
<ClinicalPanel className="p-5">
|
||||
<div className="mb-5 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-semibold">Ближайшие плановые слоты</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">Слоты показывают врача, кабинет и подготовку, а не просто время.</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="w-fit rounded-md border-primary/30 text-primary">
|
||||
<ClockIcon className="size-3.5" />
|
||||
не для острых состояний
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
{scheduleRows.map((row) => (
|
||||
<div key={`${row.service}-${row.time}`} className="grid gap-3 rounded-md border border-border bg-background p-4 lg:grid-cols-[1fr_190px_120px_180px] lg:items-center">
|
||||
<div>
|
||||
<div className="font-semibold">{row.service}</div>
|
||||
<div className="mt-1 text-sm text-muted-foreground">{row.specialist}</div>
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-primary">{row.time}</div>
|
||||
<div className="text-sm text-muted-foreground">{row.room}</div>
|
||||
<div className="text-sm text-muted-foreground">{row.prep}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ClinicalPanel>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function DoctorGrid() {
|
||||
return (
|
||||
<Container className="grid gap-4 py-12 md:grid-cols-2 xl:grid-cols-4">
|
||||
{doctors.map((doctor) => (
|
||||
<ClinicalPanel key={doctor.name} className="p-6">
|
||||
<div className="mb-7 flex size-14 items-center justify-center rounded-md bg-primary/10 text-primary">
|
||||
<UserRoundIcon className="size-7" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold">{doctor.name}</h3>
|
||||
<p className="mt-2 text-sm font-semibold text-primary">{doctor.role}</p>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{doctor.focus}</p>
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
<Badge variant="outline" className="rounded-md">{doctor.experience}</Badge>
|
||||
<Badge variant="secondary" className="rounded-md">{doctor.nextSlot}</Badge>
|
||||
</div>
|
||||
</ClinicalPanel>
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function PricingTiles({ title, items = visitFormats }: { title: string; items?: readonly TileItem[] }) {
|
||||
return (
|
||||
<Container className="py-14">
|
||||
<h2 className="mb-7 text-4xl font-semibold sm:text-5xl">{title}</h2>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{items.map((item) => (
|
||||
<ClinicalPanel key={item.title} className="p-6">
|
||||
<h3 className="text-2xl font-semibold">{item.title}</h3>
|
||||
<div className="mt-3 text-3xl font-semibold text-primary">{item.price}</div>
|
||||
<Separator className="my-5" />
|
||||
<div className="grid gap-2">
|
||||
{item.items.map((line) => (
|
||||
<div key={line} className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<CheckIcon className="size-4 text-primary" />
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ClinicalPanel>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconCards({ items = carePillars }: { items?: readonly IconItem[] }) {
|
||||
return (
|
||||
<Container className="grid gap-4 py-12 md:grid-cols-3">
|
||||
{items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<ClinicalPanel key={item.title} className="p-6">
|
||||
<Icon className="mb-8 size-6 text-primary" />
|
||||
<h3 className="text-xl font-semibold">{item.title}</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
||||
</ClinicalPanel>
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function PatientChecklist() {
|
||||
return (
|
||||
<Container className="py-14">
|
||||
<SectionHeader
|
||||
eyebrow="Before visit"
|
||||
title="Пациентская подготовка без скрытых ожиданий"
|
||||
text="Эта секция закрывает один из главных вопросов медицинского сайта: что взять с собой и почему это нужно."
|
||||
/>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{patientChecklist.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<ClinicalPanel key={item.title} className="p-6">
|
||||
<Icon className="mb-8 size-6 text-primary" />
|
||||
<h3 className="text-xl font-semibold">{item.title}</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
||||
</ClinicalPanel>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function ReservationForm() {
|
||||
return (
|
||||
<Container className="grid gap-8 py-14 lg:grid-cols-[0.82fr_1.18fr]">
|
||||
<div>
|
||||
<Badge variant="outline" className="mb-5 rounded-md border-primary/30 text-primary">
|
||||
<CalendarCheckIcon className="size-4" />
|
||||
Triage form
|
||||
</Badge>
|
||||
<h2 className="text-4xl font-semibold leading-tight sm:text-5xl">Запись начинается с цели визита</h2>
|
||||
<p className="mt-4 text-sm leading-7 text-muted-foreground">
|
||||
Форма статическая. Она показывает будущий booking flow: причина обращения, желаемый маршрут, документы и безопасное предупреждение
|
||||
о неэкстренном формате.
|
||||
</p>
|
||||
<div className="mt-6 rounded-md border border-amber-200 bg-amber-50 p-4 text-sm leading-6 text-amber-900">
|
||||
При резком ухудшении состояния, сильной боли, травме или угрозе жизни нужно обращаться в экстренные службы.
|
||||
</div>
|
||||
</div>
|
||||
<form className="grid gap-4 rounded-md border border-border bg-card p-5">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<Input placeholder="Имя пациента" />
|
||||
<Input placeholder="Телефон или email" />
|
||||
<Input placeholder="Желаемая дата" />
|
||||
<Input placeholder="Удобное время" />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="text-sm font-semibold">Причина обращения</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{bookingReasons.map((reason) => (
|
||||
<button key={reason} type="button" className="rounded-md border border-border bg-background px-3 py-2 text-sm font-medium text-muted-foreground">
|
||||
{reason}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Textarea placeholder="Кратко опишите цель визита, прошлые исследования, ограничения или вопросы координатору" />
|
||||
<Button type="button" className="h-11 rounded-md">
|
||||
Отправить заявку координатору
|
||||
</Button>
|
||||
</form>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function InsurancePanel() {
|
||||
return (
|
||||
<Container className="py-12">
|
||||
<SectionHeader
|
||||
eyebrow="Documents and privacy"
|
||||
title="Страховка, заключения и конфиденциальность вынесены в явный сервисный слой"
|
||||
text="Для клиники важно показать не только врачей, но и то, как пациент получает документы, где хранятся результаты и что происходит с оплатой."
|
||||
/>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{insuranceNotes.map((item) => (
|
||||
<ClinicalPanel key={item.title} className="p-6">
|
||||
<ShieldCheckIcon className="mb-8 size-6 text-primary" />
|
||||
<h3 className="text-xl font-semibold">{item.title}</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
||||
</ClinicalPanel>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function FacilityBoard() {
|
||||
return (
|
||||
<Container className="py-12">
|
||||
<div className="grid gap-4 lg:grid-cols-[1fr_1.05fr]">
|
||||
<ClinicalPanel className="relative min-h-[440px] overflow-hidden bg-muted">
|
||||
<Image
|
||||
src={site.accentImage}
|
||||
alt="Клиническая зона диагностики"
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(min-width: 1024px) 50vw, 100vw"
|
||||
/>
|
||||
</ClinicalPanel>
|
||||
<div className="grid gap-4">
|
||||
{facilityCards.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<ClinicalPanel key={item.title} className="p-6">
|
||||
<Icon className="mb-7 size-6 text-primary" />
|
||||
<h3 className="text-2xl font-semibold">{item.title}</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
||||
</ClinicalPanel>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function SafetyNotes() {
|
||||
return (
|
||||
<Container className="grid gap-4 py-12 md:grid-cols-3">
|
||||
{safetyNotes.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<ClinicalPanel key={item.title} className="p-6">
|
||||
<Icon className="mb-8 size-6 text-primary" />
|
||||
<h3 className="text-xl font-semibold">{item.title}</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
||||
</ClinicalPanel>
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function ContactForm() {
|
||||
return (
|
||||
<Container className="grid gap-6 py-14 lg:grid-cols-[0.95fr_1.05fr]">
|
||||
<ClinicalPanel className="p-6">
|
||||
<ClockIcon className="mb-8 size-6 text-primary" />
|
||||
<h2 className="text-4xl font-semibold">Связаться с координатором</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-muted-foreground">
|
||||
Контактная страница должна отвечать на практичные вопросы: как добраться, когда работает лабораторное окно и что взять на прием.
|
||||
</p>
|
||||
<div className="mt-6 grid gap-3">
|
||||
{contactChannels.map((channel) => (
|
||||
<div key={channel.label} className="flex items-center justify-between gap-4 rounded-md bg-muted px-4 py-3 text-sm">
|
||||
<span className="font-semibold">{channel.label}</span>
|
||||
<span className="text-muted-foreground">{channel.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ClinicalPanel>
|
||||
<form className="grid gap-3 rounded-md border border-border bg-card p-5">
|
||||
<Input placeholder="Ваше имя" />
|
||||
<Input placeholder="Email или телефон" />
|
||||
<Textarea placeholder="Вопрос координатору, документы, страховая или желаемый маршрут" />
|
||||
<Button type="button" className="h-11 rounded-md">
|
||||
Отправить сообщение
|
||||
</Button>
|
||||
</form>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function InfoColumns({ title, items }: { title: string; items: readonly InfoItem[] }) {
|
||||
return (
|
||||
<Container className="py-12">
|
||||
<h2 className="mb-6 text-4xl font-semibold">{title}</h2>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{items.map((item) => (
|
||||
<ClinicalPanel key={item.title} className="p-6">
|
||||
<FileTextIcon className="mb-8 size-5 text-primary" />
|
||||
<h3 className="text-xl font-semibold">{item.title}</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
||||
</ClinicalPanel>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function SplitStory({
|
||||
image,
|
||||
eyebrow,
|
||||
title,
|
||||
text,
|
||||
points,
|
||||
}: {
|
||||
image: string;
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
text: string;
|
||||
points: readonly string[];
|
||||
}) {
|
||||
return (
|
||||
<Container className="grid gap-10 py-14 lg:grid-cols-2">
|
||||
<ClinicalPanel className="relative min-h-[470px] overflow-hidden bg-muted">
|
||||
<Image src={image} alt={title} fill className="object-cover" sizes="(min-width: 1024px) 50vw, 100vw" />
|
||||
</ClinicalPanel>
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-primary">{eyebrow}</div>
|
||||
<h2 className="text-4xl font-semibold leading-tight sm:text-5xl">{title}</h2>
|
||||
<p className="mt-5 text-base leading-7 text-muted-foreground">{text}</p>
|
||||
<div className="mt-7 grid gap-3">
|
||||
{points.map((point) => (
|
||||
<div key={point} className="flex items-center gap-3 text-sm font-semibold">
|
||||
<CheckIcon className="size-4 text-primary" />
|
||||
{point}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function TestimonialBand({ items = testimonials }: { items?: readonly TestimonialItem[] }) {
|
||||
return (
|
||||
<Container className="grid gap-4 py-12 md:grid-cols-2">
|
||||
{items.map((item) => (
|
||||
<blockquote key={item.name} className="rounded-md border border-border bg-card p-7">
|
||||
<div className="text-sm font-semibold text-primary">{item.rating}</div>
|
||||
<p className="mt-5 text-2xl font-semibold leading-snug">"{item.text}"</p>
|
||||
<footer className="mt-5 text-sm text-muted-foreground">{item.name}</footer>
|
||||
</blockquote>
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function CtaPanel({ title, text, href, label }: { title: string; text: string; href: string; label: string }) {
|
||||
return (
|
||||
<Container className="py-12">
|
||||
<div className="rounded-md bg-primary p-8 text-primary-foreground">
|
||||
<AlertCircleIcon className="mb-8 size-6" />
|
||||
<h2 className="text-4xl font-semibold">{title}</h2>
|
||||
<p className="mt-4 max-w-[720px] text-sm leading-7 text-primary-foreground/72">{text}</p>
|
||||
<Button asChild variant="secondary" className="mt-6 rounded-md">
|
||||
<Link href={href}>
|
||||
{label}
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function ClinicalDisclaimer() {
|
||||
return (
|
||||
<Container className="py-10">
|
||||
<div className="rounded-md border border-amber-200 bg-amber-50 p-5 text-sm leading-7 text-amber-950">
|
||||
<div className="flex items-start gap-3">
|
||||
<StethoscopeIcon className="mt-1 size-5 shrink-0" />
|
||||
<p>
|
||||
Northline Clinic в этом шаблоне описывает плановый пациентский путь. Тексты не являются медицинской рекомендацией,
|
||||
не ставят диагноз и не заменяют консультацию врача.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user