feat(sprint-5.5): add page preview section with Create button
- 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>
This commit is contained in:
@@ -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 />;
|
||||
}
|
||||
Reference in New Issue
Block a user