Browse Source
- New route /pages/preview with empty state ("Создать") and assembled preview
- Preview assembles real site blocks in order (Hero + Doctors ready, rest placeholders)
- localStorage persists created state; "Пересобрать" resets it
- Extracted HeroBlock and DoctorsBlock as reusable components
- Refactored hero and doctors pages to import from components/blocks/
- Sidebar: added "Просмотр страницы" link, bumped to Sprint 5.5 · v0.5.5
- SPRINTS.md: added Sprint 5.5 plan with summary table row
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
8 changed files with 496 additions and 222 deletions
@ -0,0 +1,200 @@ |
|||||||
|
"use client"; |
||||||
|
|
||||||
|
import { useState, useEffect } from "react"; |
||||||
|
import { HeroBlock } from "@/components/blocks/HeroBlock"; |
||||||
|
import { DoctorsBlock } from "@/components/blocks/DoctorsBlock"; |
||||||
|
|
||||||
|
const STORAGE_KEY = "bb-preview-created"; |
||||||
|
|
||||||
|
function BlockPlaceholder({ name, href }: { name: string; href: string }) { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
className="w-full py-14 flex flex-col items-center justify-center rounded-xl gap-2" |
||||||
|
style={{ |
||||||
|
border: "2px dashed #d1d5db", |
||||||
|
background: "#f9fafb", |
||||||
|
}} |
||||||
|
> |
||||||
|
<p className="text-sm font-medium" style={{ color: "#6b7280" }}> |
||||||
|
{name} |
||||||
|
</p> |
||||||
|
<a |
||||||
|
href={href} |
||||||
|
className="text-xs underline" |
||||||
|
style={{ color: "var(--brand-053m)" }} |
||||||
|
> |
||||||
|
Открыть в брендбуке → |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
const BLOCKS: Array<{ |
||||||
|
id: string; |
||||||
|
name: string; |
||||||
|
href: string; |
||||||
|
ready: boolean; |
||||||
|
component?: React.ReactNode; |
||||||
|
}> = [ |
||||||
|
{ |
||||||
|
id: "hero", |
||||||
|
name: "Hero-баннер", |
||||||
|
href: "/blocks/hero", |
||||||
|
ready: true, |
||||||
|
component: <HeroBlock />, |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: "ceo", |
||||||
|
name: "Вводный текст (CEO-блок)", |
||||||
|
href: "/blocks/ceo", |
||||||
|
ready: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: "doctors", |
||||||
|
name: "Наши врачи", |
||||||
|
href: "/blocks/doctors", |
||||||
|
ready: true, |
||||||
|
component: <DoctorsBlock />, |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: "reviews", |
||||||
|
name: "Отзывы", |
||||||
|
href: "/blocks/reviews", |
||||||
|
ready: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: "contact-forms", |
||||||
|
name: "Формы записи", |
||||||
|
href: "/blocks/contact-forms", |
||||||
|
ready: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: "news", |
||||||
|
name: "Новости", |
||||||
|
href: "/blocks/news", |
||||||
|
ready: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: "footer", |
||||||
|
name: "Подвал / Контакт", |
||||||
|
href: "/blocks/contact", |
||||||
|
ready: false, |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
const READY_COUNT = BLOCKS.filter((b) => b.ready).length; |
||||||
|
|
||||||
|
export function PreviewClient() { |
||||||
|
const [created, setCreated] = useState(false); |
||||||
|
const [mounted, setMounted] = useState(false); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
setMounted(true); |
||||||
|
if (localStorage.getItem(STORAGE_KEY) === "true") { |
||||||
|
setCreated(true); |
||||||
|
} |
||||||
|
}, []); |
||||||
|
|
||||||
|
function handleCreate() { |
||||||
|
localStorage.setItem(STORAGE_KEY, "true"); |
||||||
|
setCreated(true); |
||||||
|
} |
||||||
|
|
||||||
|
function handleRebuild() { |
||||||
|
localStorage.removeItem(STORAGE_KEY); |
||||||
|
setCreated(false); |
||||||
|
} |
||||||
|
|
||||||
|
// Avoid hydration mismatch — render nothing until mounted
|
||||||
|
if (!mounted) return null; |
||||||
|
|
||||||
|
/* ── ПУСТОЕ СОСТОЯНИЕ ── */ |
||||||
|
if (!created) { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
className="flex flex-col items-center justify-center min-h-screen" |
||||||
|
style={{ background: "var(--bb-bg)" }} |
||||||
|
> |
||||||
|
<div className="text-center max-w-md space-y-5 p-8"> |
||||||
|
<p |
||||||
|
className="text-xs font-semibold uppercase tracking-widest" |
||||||
|
style={{ color: "var(--brand-053m)" }} |
||||||
|
> |
||||||
|
Страницы |
||||||
|
</p> |
||||||
|
<h1 className="text-2xl font-bold" style={{ color: "var(--bb-text)" }}> |
||||||
|
Просмотр текущей страницы |
||||||
|
</h1> |
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)", lineHeight: 1.7 }}> |
||||||
|
Нажмите «Создать», чтобы собрать главную страницу{" "} |
||||||
|
<span className="font-mono text-xs">perm.oclinica.ru/lor</span> из блоков, |
||||||
|
задокументированных в брендбуке. |
||||||
|
</p> |
||||||
|
<div |
||||||
|
className="flex items-center justify-center gap-2 text-xs px-4 py-2 rounded-lg" |
||||||
|
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }} |
||||||
|
> |
||||||
|
<span |
||||||
|
className="inline-block w-2 h-2 rounded-full" |
||||||
|
style={{ background: "#22c55e" }} |
||||||
|
/> |
||||||
|
<span style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
Готово блоков: {READY_COUNT} из {BLOCKS.length} |
||||||
|
</span> |
||||||
|
<span style={{ color: "var(--bb-text-muted)" }}>·</span> |
||||||
|
<span style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
{BLOCKS.length - READY_COUNT} плейсхолдеров |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<button onClick={handleCreate} className="bb-btn bb-btn-md bb-btn-primary"> |
||||||
|
Создать |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/* ── СОЗДАННОЕ СОСТОЯНИЕ ── */ |
||||||
|
return ( |
||||||
|
<div> |
||||||
|
{/* Топ-бар */} |
||||||
|
<div |
||||||
|
className="sticky top-0 z-10 flex items-center justify-between px-6 py-3 border-b" |
||||||
|
style={{ |
||||||
|
background: "var(--bb-sidebar-bg)", |
||||||
|
borderColor: "var(--bb-border)", |
||||||
|
}} |
||||||
|
> |
||||||
|
<div> |
||||||
|
<p |
||||||
|
className="text-xs font-semibold uppercase tracking-widest" |
||||||
|
style={{ color: "var(--brand-053m)" }} |
||||||
|
> |
||||||
|
Просмотр текущей страницы |
||||||
|
</p> |
||||||
|
<p className="text-xs mt-0.5" style={{ color: "var(--bb-text-muted)" }}> |
||||||
|
perm.oclinica.ru/lor · {READY_COUNT}/{BLOCKS.length} блоков готово |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
<button onClick={handleRebuild} className="bb-btn bb-btn-sm bb-btn-outline"> |
||||||
|
Пересобрать |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Собранная страница */} |
||||||
|
<div className="max-w-5xl mx-auto px-8 py-8 space-y-12"> |
||||||
|
{BLOCKS.map((block) => |
||||||
|
block.ready && block.component ? ( |
||||||
|
<section key={block.id}>{block.component}</section> |
||||||
|
) : ( |
||||||
|
<section key={block.id}> |
||||||
|
<BlockPlaceholder name={block.name} href={block.href} /> |
||||||
|
</section> |
||||||
|
) |
||||||
|
)} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
import type { Metadata } from "next"; |
||||||
|
import { PreviewClient } from "./PreviewClient"; |
||||||
|
|
||||||
|
export const metadata: Metadata = { |
||||||
|
title: |
||||||
|
"Просмотр текущей страницы. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", |
||||||
|
}; |
||||||
|
|
||||||
|
export default function PreviewPage() { |
||||||
|
return <PreviewClient />; |
||||||
|
} |
||||||
@ -0,0 +1,95 @@ |
|||||||
|
export const STATS = [ |
||||||
|
{ num: "27", label: "ЛОР врачей работает в клинике", prefix: "Ежедневно" }, |
||||||
|
{ num: "6", label: "кандидатов медицинских наук", prefix: "В том числе" }, |
||||||
|
{ num: "12 000+", label: "успешно проведённых операций", prefix: "Свыше" }, |
||||||
|
]; |
||||||
|
|
||||||
|
export const DOCTORS = [ |
||||||
|
{ |
||||||
|
name: "Макарова Людмила Германовна", |
||||||
|
spec: "ЛОР врач, сурдолог", |
||||||
|
photo: "/doctors/makarova.jpg", |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "Семерикова Наталия Александровна", |
||||||
|
spec: "ЛОР врач, сурдолог, хирург. К.М.Н. Завед. Центром сурдологии", |
||||||
|
photo: "/doctors/semerikova.png", |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "Ворончихина Наталия Валерьевна", |
||||||
|
spec: "Отоневролог, хирург. К.М.Н., доцент кафедры ПГМУ", |
||||||
|
photo: "/doctors/voronchikhina.png", |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "Лобанова Ирина Юрьевна", |
||||||
|
spec: "ЛОР врач, сурдолог", |
||||||
|
photo: "/doctors/lobanova.jpg", |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "Торсунова Наталья Сергеевна", |
||||||
|
spec: "Специалист по слухопротезированию (сурдоакустик)", |
||||||
|
photo: "/doctors/torsunova.jpg", |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "Суворова Светлана Викторовна", |
||||||
|
spec: "ЛОР врач, сурдолог", |
||||||
|
photo: "/doctors/suvorova.jpg", |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
export function DoctorsBlock() { |
||||||
|
return ( |
||||||
|
<div className="space-y-8"> |
||||||
|
{/* Заголовок + описание */} |
||||||
|
<div> |
||||||
|
<h2 className="text-3xl font-bold mb-3" style={{ color: "#111827" }}> |
||||||
|
Приём ведут опытные ЛОР врачи |
||||||
|
</h2> |
||||||
|
<p className="text-sm" style={{ color: "#374151", lineHeight: 1.7 }}> |
||||||
|
Фундаментальная теоретическая подготовка и большой практический опыт в сочетании |
||||||
|
с внимательным индивидуальным подходом являются причиной успеха лечения тысяч наших пациентов |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Статистика — без фона, border-bottom #60959c */} |
||||||
|
<div className="grid grid-cols-3 gap-6"> |
||||||
|
{STATS.map((s) => ( |
||||||
|
<div key={s.num} className="pb-3" style={{ borderBottom: "3px solid #60959c" }}> |
||||||
|
<p className="text-lg font-bold leading-snug" style={{ color: "#60959c" }}> |
||||||
|
{s.prefix} {s.num} {s.label} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Сетка врачей — 6 колонок */} |
||||||
|
<div className="grid grid-cols-6 gap-3"> |
||||||
|
{DOCTORS.map((doc) => ( |
||||||
|
<div key={doc.name} className="flex flex-col items-center text-center gap-1.5"> |
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */} |
||||||
|
<img |
||||||
|
src={doc.photo} |
||||||
|
alt={doc.name} |
||||||
|
style={{ |
||||||
|
width: 110, |
||||||
|
height: 150, |
||||||
|
objectFit: "cover", |
||||||
|
objectPosition: "center top", |
||||||
|
borderRadius: 4, |
||||||
|
display: "block", |
||||||
|
}} |
||||||
|
/> |
||||||
|
<div> |
||||||
|
<p className="text-xs font-medium leading-snug" style={{ color: "#60959c" }}> |
||||||
|
{doc.name} |
||||||
|
</p> |
||||||
|
<p className="text-[11px] mt-0.5 leading-snug" style={{ color: "#374151" }}> |
||||||
|
{doc.spec} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,101 @@ |
|||||||
|
export const HERO_CHECKS = [ |
||||||
|
{ key: "БЕЗОПАСНО", desc: "оперируют хирурги с 15-летним опытом работы" }, |
||||||
|
{ key: "БЕЗ ВНЕШНИХ РАЗРЕЗОВ", desc: "хирургия сверхмалых размеров" }, |
||||||
|
{ key: "БЫСТРО", desc: "под наблюдением врача пациент находится 1 сутки" }, |
||||||
|
]; |
||||||
|
|
||||||
|
export function HeroBlock() { |
||||||
|
return ( |
||||||
|
<div className="space-y-3"> |
||||||
|
{/* H1 страницы */} |
||||||
|
<h2 |
||||||
|
className="text-xl font-bold leading-snug" |
||||||
|
style={{ color: "#53514e" }} |
||||||
|
> |
||||||
|
ЛОР Клиника ухо, горло, нос – медицинский центр лечения ЛОР заболеваний у детей и взрослых |
||||||
|
</h2> |
||||||
|
|
||||||
|
{/* Баннер — единый светло-кремовый фон */} |
||||||
|
<div |
||||||
|
className="rounded-xl overflow-hidden flex" |
||||||
|
style={{ background: "#f9f4e7", minHeight: 280 }} |
||||||
|
> |
||||||
|
{/* Левая часть — контент */} |
||||||
|
<div |
||||||
|
className="flex flex-col justify-center gap-5 p-8" |
||||||
|
style={{ width: "50%", flexShrink: 0 }} |
||||||
|
> |
||||||
|
<p |
||||||
|
className="text-base font-bold uppercase leading-snug" |
||||||
|
style={{ color: "#111827" }} |
||||||
|
> |
||||||
|
Эндоскопическое хирургическое лечение ЛОР органов |
||||||
|
</p> |
||||||
|
<ul className="space-y-3"> |
||||||
|
{HERO_CHECKS.map((c) => ( |
||||||
|
<li key={c.key} className="flex items-start gap-2 text-sm"> |
||||||
|
<span |
||||||
|
className="shrink-0 font-bold" |
||||||
|
style={{ color: "#bf9975", marginTop: 1 }} |
||||||
|
> |
||||||
|
✓ |
||||||
|
</span> |
||||||
|
<span> |
||||||
|
<span className="font-bold uppercase" style={{ color: "#111827" }}> |
||||||
|
{c.key} |
||||||
|
</span>{" "} |
||||||
|
<span style={{ color: "#374151" }}>– {c.desc}</span> |
||||||
|
</span> |
||||||
|
</li> |
||||||
|
))} |
||||||
|
</ul> |
||||||
|
<div> |
||||||
|
<button className="bb-btn bb-btn-md bb-btn-outline"> |
||||||
|
Узнать стоимость операции |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Правая часть — фото врача */} |
||||||
|
<div className="flex-1 relative overflow-hidden" style={{ minHeight: 280 }}> |
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */} |
||||||
|
<img |
||||||
|
src="/hero-doctor.jpg" |
||||||
|
alt="Врач на приёме с пациентом" |
||||||
|
style={{ |
||||||
|
width: "100%", |
||||||
|
height: "100%", |
||||||
|
objectFit: "cover", |
||||||
|
objectPosition: "center top", |
||||||
|
position: "absolute", |
||||||
|
inset: 0, |
||||||
|
}} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{/* Под баннером: соцсети + просмотры */} |
||||||
|
<div className="flex items-center gap-3 pt-1"> |
||||||
|
<span className="text-xs" style={{ color: "#9ca3af" }}> |
||||||
|
Поделиться: |
||||||
|
</span> |
||||||
|
{["VK", "FB", "TW"].map((s) => ( |
||||||
|
<button |
||||||
|
key={s} |
||||||
|
className="text-xs px-2 py-1 rounded" |
||||||
|
style={{ |
||||||
|
background: "#f9fafb", |
||||||
|
border: "1px solid #e5e7eb", |
||||||
|
color: "#9ca3af", |
||||||
|
}} |
||||||
|
> |
||||||
|
{s} |
||||||
|
</button> |
||||||
|
))} |
||||||
|
<span className="text-xs ml-2" style={{ color: "#9ca3af" }}> |
||||||
|
👁 98 573 просмотра |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
Loading…
Reference in new issue