feat(sprint-5.5): add block version snapshots with switching between versions
- Add BlockSnapshot Prisma model (html, css, version, changelog) + migration - Add API endpoints: POST/GET /blocks/snapshots, GET /blocks/snapshots/:id - BlockMetaBar: version dropdown, HTML capture on save, onSnapshotSelect prop - "Сохранить версию" now captures innerHTML snapshot + CSS and stores in DB - Selecting archived version shows stored HTML snapshot instead of live component - Yellow banner "Архивная версия" with link to return to current - Split all 8 block pages into Server Component (metadata) + Client Component - Add data-block-capture attribute for snapshot capture targeting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||
import { HeroBlock, HERO_CHECKS } from "@/components/blocks/HeroBlock";
|
||||
import { BlockMetaBar, type SnapshotData } from "@/components/ui/BlockMetaBar";
|
||||
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
||||
|
||||
const BLOCK_PATH = "/blocks/hero";
|
||||
|
||||
const LLM_HERO_TEXT = `
|
||||
БЛОК: Hero-баннер (главный баннер страницы)
|
||||
Источник: perm.oclinica.ru/lor — реальный баннер раздела ЛОР
|
||||
Версия: v1.3
|
||||
|
||||
ЗАГОЛОВОК СТРАНИЦЫ (H1, над баннером):
|
||||
«ЛОР Клиника ухо, горло, нос - медицинский центр лечения ЛОР заболеваний у детей и взрослых»
|
||||
Шрифт: Fira Sans, 36px, weight 700, цвет: #cb9768
|
||||
|
||||
СТРУКТУРА БАННЕРА (двухколоночная, единый фон #f9f4e7):
|
||||
Левая колонка (~50%):
|
||||
— Заголовок: «ЭНДОСКОПИЧЕСКОЕ ХИРУРГИЧЕСКОЕ ЛЕЧЕНИЕ ЛОР ОРГАНОВ»
|
||||
Шрифт: Fira Sans, 22px, weight 700, uppercase, цвет #333333
|
||||
— 3 пункта с галочками (✓ бежевый #bf9975):
|
||||
1. «БЕЗОПАСНО – оперируют хирурги с 15-летним опытом работы»
|
||||
2. «БЕЗ ВНЕШНИХ РАЗРЕЗОВ – хирургия сверхмалых размеров»
|
||||
3. «БЫСТРО – под наблюдением врача пациент находится 1 сутки»
|
||||
Ключевое слово: uppercase bold; описание: обычный текст, ~13px
|
||||
— Кнопка «Узнать стоимость операции» — стиль: bb-btn-pill (кремовый фон #E9E4D4, radius 25px)
|
||||
Правая колонка (~50%):
|
||||
— Фото врача с пациентом
|
||||
— Изображение занимает всю высоту блока
|
||||
|
||||
ПОД БАННЕРОМ:
|
||||
— Кнопки соцсетей (VK, FB, TW), цвет #9ca3af
|
||||
— Счётчик просмотров
|
||||
|
||||
ЦВЕТА:
|
||||
Фон баннера: #f9f4e7 (светло-кремовый, единый для всего блока)
|
||||
Кнопка CTA: pill-стиль (кремовый #E9E4D4, border #D5CFBD, radius 25px)
|
||||
Заголовок блока: #333333
|
||||
Пункты: ключевое слово #111827 bold, описание #374151
|
||||
Галочка: #bf9975 (бежевый)
|
||||
|
||||
ПРАВИЛА:
|
||||
✓ Фон баннера всегда #f9f4e7 (светло-кремовый) — единый, без разделения на зоны
|
||||
✓ Заголовок блока uppercase, жирный
|
||||
✓ Три пункта с галочками обязательны
|
||||
✕ Не менять фон баннера на другой цвет
|
||||
✕ Не разбивать фон на два разных цвета
|
||||
✕ Не убирать три пункта с галочками
|
||||
`.trim();
|
||||
|
||||
const CHANGELOG: ChangelogEntry[] = [
|
||||
{
|
||||
version: "v1.3",
|
||||
date: "24.03.2026",
|
||||
changes: [
|
||||
"Счётчик: «98 573 просмотра» заменён на «Поделиться / 98572» (как на реальном сайте)",
|
||||
"Убраны кнопки VK/FB/TW",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v1.2",
|
||||
date: "24.03.2026",
|
||||
changes: [
|
||||
"H1: цвет исправлен с #53514e на #cb9768, размер с ~20px на 36px",
|
||||
"Заголовок баннера: размер с ~16px на 22px, цвет с #111827 на #333333",
|
||||
"CTA-кнопка: стиль изменён с outline на pill (фон #E9E4D4, radius 25px)",
|
||||
"Дефис в H1: длинное тире «–» заменено на простой дефис «-» (как на сайте)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v1.1",
|
||||
date: "23.03.2026",
|
||||
changes: [
|
||||
"Единый фон #f9f4e7 (ранее был разбит на две зоны)",
|
||||
"Реальное фото врача с пациентом",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function HeroPageClient() {
|
||||
const [snapshot, setSnapshot] = useState<SnapshotData | null>(null);
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-5xl mx-auto space-y-10">
|
||||
{/* Заголовок страницы */}
|
||||
<div>
|
||||
<p
|
||||
className="text-xs font-semibold uppercase tracking-widest mb-1"
|
||||
style={{ color: "var(--brand-053m)" }}
|
||||
>
|
||||
Блоки
|
||||
</p>
|
||||
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}>
|
||||
Hero-баннер
|
||||
</h1>
|
||||
<BlockMetaBar
|
||||
path={BLOCK_PATH}
|
||||
defaultVersion="v1.3"
|
||||
defaultIsInPreview={true}
|
||||
defaultChangelog={CHANGELOG}
|
||||
onSnapshotSelect={setSnapshot}
|
||||
/>
|
||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Главный баннер страницы раздела ЛОР — perm.oclinica.ru/lor. Двухколоночный блок, единый светло-кремовый фон{" "}
|
||||
<strong>#f9f4e7</strong>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Живой пример */}
|
||||
<section className="space-y-3">
|
||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||
Живой пример
|
||||
</h2>
|
||||
{snapshot ? (
|
||||
<>
|
||||
<style dangerouslySetInnerHTML={{ __html: snapshot.css }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: snapshot.html }} />
|
||||
</>
|
||||
) : (
|
||||
<div data-block-capture={BLOCK_PATH}>
|
||||
<HeroBlock />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Анатомия */}
|
||||
<section className="space-y-4">
|
||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||
Анатомия баннера
|
||||
</h2>
|
||||
<div
|
||||
className="rounded-xl p-5 space-y-2"
|
||||
style={{ background: "#f5f0e8", border: "1px solid #d5cfbd" }}
|
||||
>
|
||||
<p className="font-semibold text-sm" style={{ color: "var(--bb-text)" }}>
|
||||
Весь баннер — единый фон <span className="font-mono">#f9f4e7</span>
|
||||
</p>
|
||||
<ul className="space-y-1">
|
||||
{[
|
||||
"Фон: #f9f4e7 (светло-кремовый) — одинаковый для всего блока",
|
||||
"Левая половина (~50%): заголовок uppercase bold + 3 галочки + кнопка outline",
|
||||
"Правая половина (~50%): фото врача с пациентом",
|
||||
"Минимальная высота: ~280px",
|
||||
].map((item) => (
|
||||
<li key={item} className="text-xs flex items-start gap-1.5">
|
||||
<span style={{ color: "var(--brand-053m)" }}>·</span>
|
||||
<span style={{ color: "var(--bb-text-muted)" }}>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Три пункта с галочками */}
|
||||
<section className="space-y-3">
|
||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||
Три пункта баннера
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
{HERO_CHECKS.map((c) => (
|
||||
<div
|
||||
key={c.key}
|
||||
className="flex items-start gap-3 p-3 rounded-lg"
|
||||
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}
|
||||
>
|
||||
<span className="font-bold text-lg shrink-0" style={{ color: "#bf9975" }}>
|
||||
✓
|
||||
</span>
|
||||
<div>
|
||||
<span className="text-sm font-bold uppercase" style={{ color: "#111827" }}>
|
||||
{c.key}
|
||||
</span>
|
||||
<span className="text-sm" style={{ color: "#374151" }}>
|
||||
{" "}– {c.desc}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Ключевое слово: uppercase + bold. Описание: обычный текст. Галочка: #bf9975 (бежевый).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* LLM блок */}
|
||||
<LlmBlock path="/blocks/hero" version="v1.3" specText={LLM_HERO_TEXT}>
|
||||
<LlmSection title="Структура баннера" />
|
||||
<LlmTable
|
||||
headers={["Зона", "Ширина", "Фон", "Содержимое"]}
|
||||
rows={[
|
||||
["Весь баннер", "100%", "#f9f4e7", "Единый светло-кремовый фон"],
|
||||
["Левый блок", "~50%", "#f9f4e7 (общий)", "Заголовок uppercase + 3 галочки + кнопка pill"],
|
||||
["Правый блок", "~50%", "#f9f4e7 (общий)", "Фото врача с пациентом"],
|
||||
["Под баннером", "100%", "#fff", "Кнопки соцсетей + счётчик просмотров"],
|
||||
]}
|
||||
/>
|
||||
<LlmSection title="Три пункта баннера" />
|
||||
<LlmTable
|
||||
headers={["Ключевое слово", "Описание"]}
|
||||
rows={HERO_CHECKS.map((c) => [c.key, c.desc])}
|
||||
/>
|
||||
<LlmSection title="Цвета" />
|
||||
<LlmTable
|
||||
headers={["Элемент", "Цвет", "Токен"]}
|
||||
rows={[
|
||||
["Фон баннера (единый)", "#f9f4e7", "Светло-кремовый фон"],
|
||||
["Кнопка CTA", "pill-стиль (#E9E4D4, 25px)", "bb-btn-pill"],
|
||||
["Заголовок блока", "#333333", "—"],
|
||||
["H1 страницы", "#cb9768", "36px, bold"],
|
||||
["Галочка ✓", "#bf9975", "Бежевый"],
|
||||
]}
|
||||
/>
|
||||
<LlmSection title="Правила" />
|
||||
<LlmRules
|
||||
rules={[
|
||||
{ ok: true, text: "Фон баннера: #f9f4e7 (светло-кремовый) — единый для всего блока" },
|
||||
{ ok: true, text: "Кнопка CTA: bb-btn-pill (кремовый #E9E4D4, radius 25px)" },
|
||||
{ ok: true, text: "Заголовок: uppercase, bold" },
|
||||
{ ok: true, text: "Три пункта с галочками ✓ (#bf9975)" },
|
||||
{ ok: false, text: "Не менять фон баннера на другой цвет" },
|
||||
{ ok: false, text: "Не разбивать баннер на два разных цвета фона" },
|
||||
{ ok: false, text: "Не убирать три пункта с галочками" },
|
||||
]}
|
||||
/>
|
||||
</LlmBlock>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,214 +1,10 @@
|
||||
import type { Metadata } from "next";
|
||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||
import { HeroBlock, HERO_CHECKS } from "@/components/blocks/HeroBlock";
|
||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
||||
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
||||
import HeroPageClient from "./HeroPageClient";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Hero-баннер. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||
};
|
||||
|
||||
const LLM_HERO_TEXT = `
|
||||
БЛОК: Hero-баннер (главный баннер страницы)
|
||||
Источник: perm.oclinica.ru/lor — реальный баннер раздела ЛОР
|
||||
Версия: v1.3
|
||||
|
||||
ЗАГОЛОВОК СТРАНИЦЫ (H1, над баннером):
|
||||
«ЛОР Клиника ухо, горло, нос - медицинский центр лечения ЛОР заболеваний у детей и взрослых»
|
||||
Шрифт: Fira Sans, 36px, weight 700, цвет: #cb9768
|
||||
|
||||
СТРУКТУРА БАННЕРА (двухколоночная, единый фон #f9f4e7):
|
||||
Левая колонка (~50%):
|
||||
— Заголовок: «ЭНДОСКОПИЧЕСКОЕ ХИРУРГИЧЕСКОЕ ЛЕЧЕНИЕ ЛОР ОРГАНОВ»
|
||||
Шрифт: Fira Sans, 22px, weight 700, uppercase, цвет #333333
|
||||
— 3 пункта с галочками (✓ бежевый #bf9975):
|
||||
1. «БЕЗОПАСНО – оперируют хирурги с 15-летним опытом работы»
|
||||
2. «БЕЗ ВНЕШНИХ РАЗРЕЗОВ – хирургия сверхмалых размеров»
|
||||
3. «БЫСТРО – под наблюдением врача пациент находится 1 сутки»
|
||||
Ключевое слово: uppercase bold; описание: обычный текст, ~13px
|
||||
— Кнопка «Узнать стоимость операции» — стиль: bb-btn-pill (кремовый фон #E9E4D4, radius 25px)
|
||||
Правая колонка (~50%):
|
||||
— Фото врача с пациентом
|
||||
— Изображение занимает всю высоту блока
|
||||
|
||||
ПОД БАННЕРОМ:
|
||||
— Кнопки соцсетей (VK, FB, TW), цвет #9ca3af
|
||||
— Счётчик просмотров
|
||||
|
||||
ЦВЕТА:
|
||||
Фон баннера: #f9f4e7 (светло-кремовый, единый для всего блока)
|
||||
Кнопка CTA: pill-стиль (кремовый #E9E4D4, border #D5CFBD, radius 25px)
|
||||
Заголовок блока: #333333
|
||||
Пункты: ключевое слово #111827 bold, описание #374151
|
||||
Галочка: #bf9975 (бежевый)
|
||||
|
||||
ПРАВИЛА:
|
||||
✓ Фон баннера всегда #f9f4e7 (светло-кремовый) — единый, без разделения на зоны
|
||||
✓ Заголовок блока uppercase, жирный
|
||||
✓ Три пункта с галочками обязательны
|
||||
✕ Не менять фон баннера на другой цвет
|
||||
✕ Не разбивать фон на два разных цвета
|
||||
✕ Не убирать три пункта с галочками
|
||||
`.trim();
|
||||
|
||||
const CHANGELOG: ChangelogEntry[] = [
|
||||
{
|
||||
version: "v1.3",
|
||||
date: "24.03.2026",
|
||||
changes: [
|
||||
"Счётчик: «98 573 просмотра» заменён на «Поделиться / 98572» (как на реальном сайте)",
|
||||
"Убраны кнопки VK/FB/TW",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v1.2",
|
||||
date: "24.03.2026",
|
||||
changes: [
|
||||
"H1: цвет исправлен с #53514e на #cb9768, размер с ~20px на 36px",
|
||||
"Заголовок баннера: размер с ~16px на 22px, цвет с #111827 на #333333",
|
||||
"CTA-кнопка: стиль изменён с outline на pill (фон #E9E4D4, radius 25px)",
|
||||
"Дефис в H1: длинное тире «–» заменено на простой дефис «-» (как на сайте)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "v1.1",
|
||||
date: "23.03.2026",
|
||||
changes: [
|
||||
"Единый фон #f9f4e7 (ранее был разбит на две зоны)",
|
||||
"Реальное фото врача с пациентом",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function HeroPage() {
|
||||
return (
|
||||
<div className="p-8 max-w-5xl mx-auto space-y-10">
|
||||
{/* Заголовок страницы */}
|
||||
<div>
|
||||
<p
|
||||
className="text-xs font-semibold uppercase tracking-widest mb-1"
|
||||
style={{ color: "var(--brand-053m)" }}
|
||||
>
|
||||
Блоки
|
||||
</p>
|
||||
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}>
|
||||
Hero-баннер
|
||||
</h1>
|
||||
<BlockMetaBar path="/blocks/hero" defaultVersion="v1.3" defaultIsInPreview={true} defaultChangelog={CHANGELOG} />
|
||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Главный баннер страницы раздела ЛОР — perm.oclinica.ru/lor. Двухколоночный блок, единый светло-кремовый фон{" "}
|
||||
<strong>#f9f4e7</strong>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Живой пример */}
|
||||
<section className="space-y-3">
|
||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||
Живой пример
|
||||
</h2>
|
||||
<HeroBlock />
|
||||
</section>
|
||||
|
||||
{/* Анатомия */}
|
||||
<section className="space-y-4">
|
||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||
Анатомия баннера
|
||||
</h2>
|
||||
<div
|
||||
className="rounded-xl p-5 space-y-2"
|
||||
style={{ background: "#f5f0e8", border: "1px solid #d5cfbd" }}
|
||||
>
|
||||
<p className="font-semibold text-sm" style={{ color: "var(--bb-text)" }}>
|
||||
Весь баннер — единый фон <span className="font-mono">#f9f4e7</span>
|
||||
</p>
|
||||
<ul className="space-y-1">
|
||||
{[
|
||||
"Фон: #f9f4e7 (светло-кремовый) — одинаковый для всего блока",
|
||||
"Левая половина (~50%): заголовок uppercase bold + 3 галочки + кнопка outline",
|
||||
"Правая половина (~50%): фото врача с пациентом",
|
||||
"Минимальная высота: ~280px",
|
||||
].map((item) => (
|
||||
<li key={item} className="text-xs flex items-start gap-1.5">
|
||||
<span style={{ color: "var(--brand-053m)" }}>·</span>
|
||||
<span style={{ color: "var(--bb-text-muted)" }}>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Три пункта с галочками */}
|
||||
<section className="space-y-3">
|
||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||
Три пункта баннера
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
{HERO_CHECKS.map((c) => (
|
||||
<div
|
||||
key={c.key}
|
||||
className="flex items-start gap-3 p-3 rounded-lg"
|
||||
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}
|
||||
>
|
||||
<span className="font-bold text-lg shrink-0" style={{ color: "#bf9975" }}>
|
||||
✓
|
||||
</span>
|
||||
<div>
|
||||
<span className="text-sm font-bold uppercase" style={{ color: "#111827" }}>
|
||||
{c.key}
|
||||
</span>
|
||||
<span className="text-sm" style={{ color: "#374151" }}>
|
||||
{" "}– {c.desc}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Ключевое слово: uppercase + bold. Описание: обычный текст. Галочка: #bf9975 (бежевый).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* LLM блок */}
|
||||
<LlmBlock path="/blocks/hero" version="v1.3" specText={LLM_HERO_TEXT}>
|
||||
<LlmSection title="Структура баннера" />
|
||||
<LlmTable
|
||||
headers={["Зона", "Ширина", "Фон", "Содержимое"]}
|
||||
rows={[
|
||||
["Весь баннер", "100%", "#f9f4e7", "Единый светло-кремовый фон"],
|
||||
["Левый блок", "~50%", "#f9f4e7 (общий)", "Заголовок uppercase + 3 галочки + кнопка pill"],
|
||||
["Правый блок", "~50%", "#f9f4e7 (общий)", "Фото врача с пациентом"],
|
||||
["Под баннером", "100%", "#fff", "Кнопки соцсетей + счётчик просмотров"],
|
||||
]}
|
||||
/>
|
||||
<LlmSection title="Три пункта баннера" />
|
||||
<LlmTable
|
||||
headers={["Ключевое слово", "Описание"]}
|
||||
rows={HERO_CHECKS.map((c) => [c.key, c.desc])}
|
||||
/>
|
||||
<LlmSection title="Цвета" />
|
||||
<LlmTable
|
||||
headers={["Элемент", "Цвет", "Токен"]}
|
||||
rows={[
|
||||
["Фон баннера (единый)", "#f9f4e7", "Светло-кремовый фон"],
|
||||
["Кнопка CTA", "pill-стиль (#E9E4D4, 25px)", "bb-btn-pill"],
|
||||
["Заголовок блока", "#333333", "—"],
|
||||
["H1 страницы", "#cb9768", "36px, bold"],
|
||||
["Галочка ✓", "#bf9975", "Бежевый"],
|
||||
]}
|
||||
/>
|
||||
<LlmSection title="Правила" />
|
||||
<LlmRules
|
||||
rules={[
|
||||
{ ok: true, text: "Фон баннера: #f9f4e7 (светло-кремовый) — единый для всего блока" },
|
||||
{ ok: true, text: "Кнопка CTA: bb-btn-pill (кремовый #E9E4D4, radius 25px)" },
|
||||
{ ok: true, text: "Заголовок: uppercase, bold" },
|
||||
{ ok: true, text: "Три пункта с галочками ✓ (#bf9975)" },
|
||||
{ ok: false, text: "Не менять фон баннера на другой цвет" },
|
||||
{ ok: false, text: "Не разбивать баннер на два разных цвета фона" },
|
||||
{ ok: false, text: "Не убирать три пункта с галочками" },
|
||||
]}
|
||||
/>
|
||||
</LlmBlock>
|
||||
</div>
|
||||
);
|
||||
return <HeroPageClient />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user