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,18 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "BlockSnapshot" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"blockPath" TEXT NOT NULL,
|
||||||
|
"version" TEXT NOT NULL,
|
||||||
|
"changelog" JSONB NOT NULL DEFAULT '[]',
|
||||||
|
"html" TEXT NOT NULL,
|
||||||
|
"css" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "BlockSnapshot_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "BlockSnapshot_blockPath_idx" ON "BlockSnapshot"("blockPath");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "BlockSnapshot_blockPath_version_key" ON "BlockSnapshot"("blockPath", "version");
|
||||||
@@ -50,3 +50,16 @@ model Block {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model BlockSnapshot {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
blockPath String
|
||||||
|
version String
|
||||||
|
changelog Json @default("[]")
|
||||||
|
html String @db.Text
|
||||||
|
css String @db.Text
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([blockPath, version])
|
||||||
|
@@index([blockPath])
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Controller, Get, Patch, Query, Body } from '@nestjs/common';
|
import { Controller, Get, Post, Patch, Param, Query, Body } from '@nestjs/common';
|
||||||
import { BlocksService } from './blocks.service';
|
import { BlocksService } from './blocks.service';
|
||||||
|
|
||||||
@Controller('blocks')
|
@Controller('blocks')
|
||||||
@@ -22,4 +22,22 @@ export class BlocksController {
|
|||||||
) {
|
) {
|
||||||
return this.blocks.update(path, body);
|
return this.blocks.update(path, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('snapshots')
|
||||||
|
createSnapshot(
|
||||||
|
@Query('path') path: string,
|
||||||
|
@Body() body: { version: string; changelog: object[]; html: string; css: string },
|
||||||
|
) {
|
||||||
|
return this.blocks.createSnapshot(path, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('snapshots')
|
||||||
|
listSnapshots(@Query('path') path: string) {
|
||||||
|
return this.blocks.listSnapshots(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('snapshots/:id')
|
||||||
|
getSnapshot(@Param('id') id: string) {
|
||||||
|
return this.blocks.getSnapshot(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,4 +20,24 @@ export class BlocksService {
|
|||||||
update(path: string, data: { version?: string; isInPreview?: boolean; changelog?: object[] }) {
|
update(path: string, data: { version?: string; isInPreview?: boolean; changelog?: object[] }) {
|
||||||
return this.prisma.block.update({ where: { path }, data });
|
return this.prisma.block.update({ where: { path }, data });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createSnapshot(blockPath: string, data: { version: string; changelog: object[]; html: string; css: string }) {
|
||||||
|
return this.prisma.blockSnapshot.upsert({
|
||||||
|
where: { blockPath_version: { blockPath, version: data.version } },
|
||||||
|
update: { html: data.html, css: data.css, changelog: data.changelog },
|
||||||
|
create: { blockPath, ...data },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
listSnapshots(blockPath: string) {
|
||||||
|
return this.prisma.blockSnapshot.findMany({
|
||||||
|
where: { blockPath },
|
||||||
|
select: { id: true, version: true, createdAt: true },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getSnapshot(id: string) {
|
||||||
|
return this.prisma.blockSnapshot.findUniqueOrThrow({ where: { id } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||||
|
import { BlockMetaBar, type SnapshotData } from "@/components/ui/BlockMetaBar";
|
||||||
|
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
||||||
|
import { CeoBlock, CEO_QUESTIONS } from "@/components/blocks/CeoBlock";
|
||||||
|
|
||||||
|
const BLOCK_PATH = "/blocks/ceo";
|
||||||
|
|
||||||
|
const LLM_CEO_TEXT = `
|
||||||
|
БЛОК: Вводный текст клиники (CEO-блок)
|
||||||
|
Источник: perm.oclinica.ru/lor — секция после баннера
|
||||||
|
Версия: v1.1
|
||||||
|
|
||||||
|
НАЗНАЧЕНИЕ:
|
||||||
|
Информационный блок под hero-баннером. Рассказывает о специализации клиники
|
||||||
|
и вовлекает пациента через вопросы-стимулы.
|
||||||
|
|
||||||
|
СТРУКТУРА:
|
||||||
|
1. Вводный абзац
|
||||||
|
«Клиника ухо, нос специализируется на оториноларингологии – лечении взрослых и детей
|
||||||
|
с ЛОР заболеваниями. ЛОР клиника представлена на двух адресах: ул. Цитная, 9, ул. Г. Звезда, 31а...»
|
||||||
|
|
||||||
|
2. Вопросы-стимулы (формат: «— Вопрос?»)
|
||||||
|
Каждый вопрос — отдельный абзац с тире, связанный с симптомами пациентов:
|
||||||
|
— У вас болит ухо, заложен нос, першит в горле, и вы не можете понять причину?
|
||||||
|
— Вам срочно нужен платный ЛОР в Перми или, как ещё говорят, «ухогорлонос»?
|
||||||
|
— Заболел ребёнок?
|
||||||
|
— Срочно ищете частные ЛОР-клиники Перми для детей 0+ и взрослых...
|
||||||
|
— Вам назначили проведение эндоскопической операции на ухе, горле или носе?
|
||||||
|
|
||||||
|
3. Заключительный абзац
|
||||||
|
«Обращайтесь в ЛОР центр ухо, горло, нос в Перми...»
|
||||||
|
«Будьте здоровы!»
|
||||||
|
|
||||||
|
ТИПОГРАФИКА:
|
||||||
|
Шрифт: Fira Sans, 14px, line-height 1.6–1.8
|
||||||
|
Цвет текста: #374151 (#bb-text)
|
||||||
|
Вопросы: тот же стиль, начинаются с «—»
|
||||||
|
Ключевые слова в тексте — обычно ссылки синего цвета (#0089c3)
|
||||||
|
Фон блока: #ffffff, отступы 40–60px сверху и снизу
|
||||||
|
|
||||||
|
ПРАВИЛА:
|
||||||
|
✓ Вопросы начинаются с «—» (тире)
|
||||||
|
✓ Ключевые медицинские термины — ссылки #0089c3
|
||||||
|
✓ Текст без H2 заголовка — просто связный параграф
|
||||||
|
✕ Не добавлять маркированные списки (только тире)
|
||||||
|
✕ Не менять стиль вопросов на другой формат
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
|
||||||
|
const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
version: "v1.1",
|
||||||
|
date: "24.03.2026",
|
||||||
|
changes: [
|
||||||
|
"Адрес: «ул. Цитная, 9» заменён на «ул. Клары Цеткин, 9»",
|
||||||
|
"Цвет ссылок: #52b4bd (было #0089c3)",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function CeoPageClient() {
|
||||||
|
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)" }}>
|
||||||
|
Вводный текст (CEO-блок)
|
||||||
|
</h1>
|
||||||
|
<BlockMetaBar
|
||||||
|
path={BLOCK_PATH}
|
||||||
|
defaultVersion="v1.1"
|
||||||
|
defaultIsInPreview={false}
|
||||||
|
defaultChangelog={CHANGELOG}
|
||||||
|
onSnapshotSelect={setSnapshot}
|
||||||
|
/>
|
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
Блок после hero-баннера на perm.oclinica.ru/lor. Описание специализации клиники
|
||||||
|
+ вопросы-стимулы для пациентов.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Живой пример */}
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||||
|
Живой пример
|
||||||
|
</h2>
|
||||||
|
{snapshot ? (
|
||||||
|
<div className="rounded-xl overflow-hidden" style={{ border: "1px solid var(--bb-border)" }}>
|
||||||
|
<style dangerouslySetInnerHTML={{ __html: snapshot.css }} />
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: snapshot.html }} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div data-block-capture={BLOCK_PATH} className="rounded-xl overflow-hidden" style={{ border: "1px solid var(--bb-border)" }}>
|
||||||
|
<CeoBlock />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Анатомия */}
|
||||||
|
<section className="space-y-4">
|
||||||
|
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||||
|
Структура блока
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
num: "1",
|
||||||
|
title: "Вводный абзац",
|
||||||
|
bg: "#f0f9ff",
|
||||||
|
desc: 'Специализация клиники, адреса. Ключевые слова — ссылки синего цвета (#0089c3).',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
num: "2",
|
||||||
|
title: "Вопросы-стимулы",
|
||||||
|
bg: "#fefce8",
|
||||||
|
desc: 'Каждый вопрос — отдельный абзац, начинается с «—». Адресован пациенту с конкретным симптомом.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
num: "3",
|
||||||
|
title: "Заключительный абзац",
|
||||||
|
bg: "#f0fdf4",
|
||||||
|
desc: 'Призыв обращаться. «Будьте здоровы!» — фирменная подпись клиники.',
|
||||||
|
},
|
||||||
|
].map((s) => (
|
||||||
|
<div
|
||||||
|
key={s.num}
|
||||||
|
className="flex gap-4 p-4 rounded-xl"
|
||||||
|
style={{ background: s.bg, border: "1px solid var(--bb-border)" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-7 h-7 rounded-full flex items-center justify-center font-bold text-sm shrink-0"
|
||||||
|
style={{ background: "var(--brand-053m)", color: "#fff" }}
|
||||||
|
>
|
||||||
|
{s.num}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-sm mb-1" style={{ color: "var(--bb-text)" }}>
|
||||||
|
{s.title}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
{s.desc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Вопросы-стимулы */}
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||||
|
Вопросы-стимулы (5 штук)
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{CEO_QUESTIONS.map((q, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="flex items-start gap-3 p-3 rounded-lg text-sm"
|
||||||
|
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}
|
||||||
|
>
|
||||||
|
<span className="font-bold shrink-0 mt-0.5" style={{ color: "var(--brand-053m)" }}>
|
||||||
|
—
|
||||||
|
</span>
|
||||||
|
<span style={{ color: "var(--bb-text)" }}>{q}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* LLM блок */}
|
||||||
|
<LlmBlock path="/blocks/ceo" version="v1.1" specText={LLM_CEO_TEXT}>
|
||||||
|
<LlmSection title="Структура блока" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Часть", "Содержимое", "Примечание"]}
|
||||||
|
rows={[
|
||||||
|
["Вводный абзац", "Специализация клиники, адреса", "Ключевые слова = ссылки #0089c3"],
|
||||||
|
["Вопросы-стимулы", "5 вопросов от лица пациента, начинаются с «—»", "Без маркированных списков"],
|
||||||
|
["Заключение", 'Призыв обращаться + «Будьте здоровы!»', "Фирменная подпись клиники"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Типографика" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Параметр", "Значение"]}
|
||||||
|
rows={[
|
||||||
|
["Шрифт", "Fira Sans"],
|
||||||
|
["Размер", "14px"],
|
||||||
|
["Line-height", "1.6–1.8"],
|
||||||
|
["Цвет текста", "#374151 (--bb-text)"],
|
||||||
|
["Цвет ссылок в тексте", "#0089c3 (--brand-053m)"],
|
||||||
|
["Фон блока", "#ffffff"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Правила" />
|
||||||
|
<LlmRules
|
||||||
|
rules={[
|
||||||
|
{ ok: true, text: "Вопросы начинаются с «—» (длинное тире)" },
|
||||||
|
{ ok: true, text: "Ключевые медицинские термины — ссылки #0089c3" },
|
||||||
|
{ ok: true, text: "Фирменная подпись «Будьте здоровы!» в конце" },
|
||||||
|
{ ok: false, text: "Не добавлять маркированные списки (•)" },
|
||||||
|
{ ok: false, text: "Не добавлять H2 заголовок внутри блока" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</LlmBlock>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,202 +1,10 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
import CeoPageClient from "./CeoPageClient";
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
|
||||||
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
|
||||||
import { CeoBlock, CEO_QUESTIONS } from "@/components/blocks/CeoBlock";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Вводный текст (CEO-блок). Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
title: "Вводный текст (CEO-блок). Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
};
|
};
|
||||||
|
|
||||||
const LLM_CEO_TEXT = `
|
|
||||||
БЛОК: Вводный текст клиники (CEO-блок)
|
|
||||||
Источник: perm.oclinica.ru/lor — секция после баннера
|
|
||||||
Версия: v1.1
|
|
||||||
|
|
||||||
НАЗНАЧЕНИЕ:
|
|
||||||
Информационный блок под hero-баннером. Рассказывает о специализации клиники
|
|
||||||
и вовлекает пациента через вопросы-стимулы.
|
|
||||||
|
|
||||||
СТРУКТУРА:
|
|
||||||
1. Вводный абзац
|
|
||||||
«Клиника ухо, нос специализируется на оториноларингологии – лечении взрослых и детей
|
|
||||||
с ЛОР заболеваниями. ЛОР клиника представлена на двух адресах: ул. Цитная, 9, ул. Г. Звезда, 31а...»
|
|
||||||
|
|
||||||
2. Вопросы-стимулы (формат: «— Вопрос?»)
|
|
||||||
Каждый вопрос — отдельный абзац с тире, связанный с симптомами пациентов:
|
|
||||||
— У вас болит ухо, заложен нос, першит в горле, и вы не можете понять причину?
|
|
||||||
— Вам срочно нужен платный ЛОР в Перми или, как ещё говорят, «ухогорлонос»?
|
|
||||||
— Заболел ребёнок?
|
|
||||||
— Срочно ищете частные ЛОР-клиники Перми для детей 0+ и взрослых...
|
|
||||||
— Вам назначили проведение эндоскопической операции на ухе, горле или носе?
|
|
||||||
|
|
||||||
3. Заключительный абзац
|
|
||||||
«Обращайтесь в ЛОР центр ухо, горло, нос в Перми...»
|
|
||||||
«Будьте здоровы!»
|
|
||||||
|
|
||||||
ТИПОГРАФИКА:
|
|
||||||
Шрифт: Fira Sans, 14px, line-height 1.6–1.8
|
|
||||||
Цвет текста: #374151 (#bb-text)
|
|
||||||
Вопросы: тот же стиль, начинаются с «—»
|
|
||||||
Ключевые слова в тексте — обычно ссылки синего цвета (#0089c3)
|
|
||||||
Фон блока: #ffffff, отступы 40–60px сверху и снизу
|
|
||||||
|
|
||||||
ПРАВИЛА:
|
|
||||||
✓ Вопросы начинаются с «—» (тире)
|
|
||||||
✓ Ключевые медицинские термины — ссылки #0089c3
|
|
||||||
✓ Текст без H2 заголовка — просто связный параграф
|
|
||||||
✕ Не добавлять маркированные списки (только тире)
|
|
||||||
✕ Не менять стиль вопросов на другой формат
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
|
|
||||||
const CHANGELOG: ChangelogEntry[] = [
|
|
||||||
{
|
|
||||||
version: "v1.1",
|
|
||||||
date: "24.03.2026",
|
|
||||||
changes: [
|
|
||||||
"Адрес: «ул. Цитная, 9» заменён на «ул. Клары Цеткин, 9»",
|
|
||||||
"Цвет ссылок: #52b4bd (было #0089c3)",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function CeoPage() {
|
export default function CeoPage() {
|
||||||
return (
|
return <CeoPageClient />;
|
||||||
<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)" }}>
|
|
||||||
Вводный текст (CEO-блок)
|
|
||||||
</h1>
|
|
||||||
<BlockMetaBar path="/blocks/ceo" defaultVersion="v1.1" defaultIsInPreview={false} defaultChangelog={CHANGELOG} />
|
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
Блок после hero-баннера на perm.oclinica.ru/lor. Описание специализации клиники
|
|
||||||
+ вопросы-стимулы для пациентов.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Живой пример */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Живой пример
|
|
||||||
</h2>
|
|
||||||
<div className="rounded-xl overflow-hidden" style={{ border: "1px solid var(--bb-border)" }}>
|
|
||||||
<CeoBlock />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Анатомия */}
|
|
||||||
<section className="space-y-4">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Структура блока
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{[
|
|
||||||
{
|
|
||||||
num: "1",
|
|
||||||
title: "Вводный абзац",
|
|
||||||
bg: "#f0f9ff",
|
|
||||||
desc: 'Специализация клиники, адреса. Ключевые слова — ссылки синего цвета (#0089c3).',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
num: "2",
|
|
||||||
title: "Вопросы-стимулы",
|
|
||||||
bg: "#fefce8",
|
|
||||||
desc: 'Каждый вопрос — отдельный абзац, начинается с «—». Адресован пациенту с конкретным симптомом.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
num: "3",
|
|
||||||
title: "Заключительный абзац",
|
|
||||||
bg: "#f0fdf4",
|
|
||||||
desc: 'Призыв обращаться. «Будьте здоровы!» — фирменная подпись клиники.',
|
|
||||||
},
|
|
||||||
].map((s) => (
|
|
||||||
<div
|
|
||||||
key={s.num}
|
|
||||||
className="flex gap-4 p-4 rounded-xl"
|
|
||||||
style={{ background: s.bg, border: "1px solid var(--bb-border)" }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="w-7 h-7 rounded-full flex items-center justify-center font-bold text-sm shrink-0"
|
|
||||||
style={{ background: "var(--brand-053m)", color: "#fff" }}
|
|
||||||
>
|
|
||||||
{s.num}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold text-sm mb-1" style={{ color: "var(--bb-text)" }}>
|
|
||||||
{s.title}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
{s.desc}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Вопросы-стимулы */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Вопросы-стимулы (5 штук)
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{CEO_QUESTIONS.map((q, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="flex items-start gap-3 p-3 rounded-lg text-sm"
|
|
||||||
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}
|
|
||||||
>
|
|
||||||
<span className="font-bold shrink-0 mt-0.5" style={{ color: "var(--brand-053m)" }}>
|
|
||||||
—
|
|
||||||
</span>
|
|
||||||
<span style={{ color: "var(--bb-text)" }}>{q}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* LLM блок */}
|
|
||||||
<LlmBlock path="/blocks/ceo" version="v1.1" specText={LLM_CEO_TEXT}>
|
|
||||||
<LlmSection title="Структура блока" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Часть", "Содержимое", "Примечание"]}
|
|
||||||
rows={[
|
|
||||||
["Вводный абзац", "Специализация клиники, адреса", "Ключевые слова = ссылки #0089c3"],
|
|
||||||
["Вопросы-стимулы", "5 вопросов от лица пациента, начинаются с «—»", "Без маркированных списков"],
|
|
||||||
["Заключение", 'Призыв обращаться + «Будьте здоровы!»', "Фирменная подпись клиники"],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Типографика" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Параметр", "Значение"]}
|
|
||||||
rows={[
|
|
||||||
["Шрифт", "Fira Sans"],
|
|
||||||
["Размер", "14px"],
|
|
||||||
["Line-height", "1.6–1.8"],
|
|
||||||
["Цвет текста", "#374151 (--bb-text)"],
|
|
||||||
["Цвет ссылок в тексте", "#0089c3 (--brand-053m)"],
|
|
||||||
["Фон блока", "#ffffff"],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Правила" />
|
|
||||||
<LlmRules
|
|
||||||
rules={[
|
|
||||||
{ ok: true, text: "Вопросы начинаются с «—» (длинное тире)" },
|
|
||||||
{ ok: true, text: "Ключевые медицинские термины — ссылки #0089c3" },
|
|
||||||
{ ok: true, text: "Фирменная подпись «Будьте здоровы!» в конце" },
|
|
||||||
{ ok: false, text: "Не добавлять маркированные списки (•)" },
|
|
||||||
{ ok: false, text: "Не добавлять H2 заголовок внутри блока" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</LlmBlock>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||||
|
import { BlockMetaBar, type SnapshotData } from "@/components/ui/BlockMetaBar";
|
||||||
|
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
||||||
|
import { ContactFormsBlock } from "@/components/blocks/ContactFormsBlock";
|
||||||
|
|
||||||
|
const BLOCK_PATH = "/blocks/contact-forms";
|
||||||
|
|
||||||
|
const LLM_FORMS_TEXT = `
|
||||||
|
БЛОК: Формы записи и обратной связи
|
||||||
|
Источник: perm.oclinica.ru/lor — два блока форм на одной странице
|
||||||
|
Версия: v1.2
|
||||||
|
|
||||||
|
ФОРМА 1: «Будьте здоровы!» (форма записи на приём)
|
||||||
|
Позиция: после блока отзывов
|
||||||
|
Заголовок H2: «Будьте здоровы!» — 36px, bold, #000000, line-height 38px
|
||||||
|
Подзаголовок: «Запишитесь на приём к врачу!»
|
||||||
|
Фон секции: #d4f6f8 (светло-бирюзовый)
|
||||||
|
Поля формы:
|
||||||
|
1. Текстовый input: placeholder «Введите ваше имя» (height 50px)
|
||||||
|
2. Телефонный input: placeholder «Введите ваш телефон» (height 50px)
|
||||||
|
3. Select: «Выберите ЛОР врача» (height 50px)
|
||||||
|
4. Checkbox: «Отправляя данные, я даю согласие на обработку персональных данных»
|
||||||
|
Кнопка: «Запишите меня!» — стиль bb-btn-primary (#FFA39C)
|
||||||
|
Ширина формы: ~400px центрирована или в колонку
|
||||||
|
|
||||||
|
ФОРМА 2: «Узнайте стоимость операции» (консультация)
|
||||||
|
Позиция: после блока новостей
|
||||||
|
Заголовок H2: «Узнайте стоимость операции» — 36px, bold, #000000, line-height 38px
|
||||||
|
Подзаголовок: «Проконсультируйтесь с ассистентом хирурга»
|
||||||
|
Фон секции: #d4f6f8 (тот же что и форма 1)
|
||||||
|
Поля формы:
|
||||||
|
1. Текстовый input: placeholder «каша» / «Введите ваше имя» (height 50px)
|
||||||
|
2. Телефонный input: placeholder «Введите ваш телефон» (height 50px)
|
||||||
|
3. Checkbox: «Отправляя данные, я даю согласие на обработку персональных данных»
|
||||||
|
Кнопка: «Перезвоните мне» — стиль bb-btn-primary (#FFA39C)
|
||||||
|
|
||||||
|
ОБЩИЕ ПРАВИЛА:
|
||||||
|
— Оба поля input/select: bb-input / bb-select (height 50px, border 1px solid #ccc)
|
||||||
|
— Чекбокс обязателен в обеих формах
|
||||||
|
— Кнопка отправки: всегда bb-btn-primary (#FFA39C)
|
||||||
|
— Обе формы на бирюзовом фоне (#d4f6f8)
|
||||||
|
|
||||||
|
ПРАВИЛА:
|
||||||
|
✓ Чекбокс согласия обязателен в каждой форме
|
||||||
|
✓ Кнопки отправки: bb-btn-primary (#FFA39C)
|
||||||
|
✓ Обе формы на фоне #d4f6f8
|
||||||
|
✕ Не менять порядок полей
|
||||||
|
✕ Не убирать чекбокс согласия
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
version: "v1.2",
|
||||||
|
date: "24.03.2026",
|
||||||
|
changes: [
|
||||||
|
"Кнопка: bb-btn-md заменена на bb-btn-lg (18px, bold, как на реальном сайте)",
|
||||||
|
"border-radius кнопки: 7px исправлен на 4px (исправлено в globals.css)",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "v1.1",
|
||||||
|
date: "24.03.2026",
|
||||||
|
changes: [
|
||||||
|
"H2: размер на 36px, цвет на #000000, line-height 38px",
|
||||||
|
"Фон формы 1: с #b8e6ed на #d4f6f8",
|
||||||
|
"Фон формы 2: с #ffffff на #d4f6f8 (обе формы на одном фоне)",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ContactFormsPageClient() {
|
||||||
|
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)" }}>
|
||||||
|
Формы записи
|
||||||
|
</h1>
|
||||||
|
<BlockMetaBar
|
||||||
|
path={BLOCK_PATH}
|
||||||
|
defaultVersion="v1.2"
|
||||||
|
defaultIsInPreview={false}
|
||||||
|
defaultChangelog={CHANGELOG}
|
||||||
|
onSnapshotSelect={setSnapshot}
|
||||||
|
/>
|
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
Два блока форм с perm.oclinica.ru/lor — запись на приём и запрос стоимости операции.
|
||||||
|
</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}>
|
||||||
|
<ContactFormsBlock />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Сравнение двух форм */}
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||||
|
Сравнение двух форм
|
||||||
|
</h2>
|
||||||
|
<div className="overflow-x-auto rounded-xl" style={{ border: "1px solid var(--bb-border)" }}>
|
||||||
|
<table className="w-full text-sm border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr style={{ background: "var(--bb-sidebar-bg)" }}>
|
||||||
|
<th className="text-left px-4 py-3 text-xs font-semibold" style={{ color: "var(--bb-text-muted)", borderBottom: "1px solid var(--bb-border)" }}>Параметр</th>
|
||||||
|
<th className="text-left px-4 py-3 text-xs font-semibold" style={{ color: "var(--bb-text-muted)", borderBottom: "1px solid var(--bb-border)" }}>Форма 1 «Будьте здоровы!»</th>
|
||||||
|
<th className="text-left px-4 py-3 text-xs font-semibold" style={{ color: "var(--bb-text-muted)", borderBottom: "1px solid var(--bb-border)" }}>Форма 2 «Стоимость операции»</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
["Заголовок", "«Будьте здоровы!»", "«Узнайте стоимость операции»"],
|
||||||
|
["Подзаголовок", "«Запишитесь на приём к врачу!»", "«Проконсультируйтесь с ассистентом хирурга»"],
|
||||||
|
["Фон секции", "#d4f6f8", "#d4f6f8"],
|
||||||
|
["Поля", "Имя + Телефон + Select врача + Checkbox", "Имя + Телефон + Checkbox"],
|
||||||
|
["Кнопка", "«Запишите меня!»", "«Перезвоните мне»"],
|
||||||
|
["Стиль кнопки", "bb-btn-primary (#FFA39C)", "bb-btn-primary (#FFA39C)"],
|
||||||
|
].map(([param, f1, f2]) => (
|
||||||
|
<tr key={param} style={{ borderTop: "1px solid var(--bb-border)" }}>
|
||||||
|
<td className="px-4 py-2 font-medium text-xs" style={{ color: "var(--bb-text)" }}>{param}</td>
|
||||||
|
<td className="px-4 py-2 text-xs" style={{ color: "var(--bb-text-muted)" }}>{f1}</td>
|
||||||
|
<td className="px-4 py-2 text-xs" style={{ color: "var(--bb-text-muted)" }}>{f2}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* LLM блок */}
|
||||||
|
<LlmBlock path="/blocks/contact-forms" version="v1.2" specText={LLM_FORMS_TEXT}>
|
||||||
|
<LlmSection title="Форма 1 — Запись на приём" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Поле", "Тип", "Placeholder"]}
|
||||||
|
rows={[
|
||||||
|
["Имя", "input[type=text]", "«Введите ваше имя»"],
|
||||||
|
["Телефон", "input[type=tel]", "«Введите ваш телефон»"],
|
||||||
|
["Врач", "select", "«Выберите ЛОР врача»"],
|
||||||
|
["Согласие", "checkbox", "«Отправляя данные, я даю согласие...»"],
|
||||||
|
["Кнопка", "button bb-btn-primary", "«Запишите меня!»"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Форма 2 — Запрос стоимости" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Поле", "Тип", "Placeholder"]}
|
||||||
|
rows={[
|
||||||
|
["Имя", "input[type=text]", "«Введите ваше имя»"],
|
||||||
|
["Телефон", "input[type=tel]", "«Введите ваш телефон»"],
|
||||||
|
["Согласие", "checkbox", "«Отправляя данные, я даю согласие...»"],
|
||||||
|
["Кнопка", "button bb-btn-primary", "«Перезвоните мне»"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Правила" />
|
||||||
|
<LlmRules
|
||||||
|
rules={[
|
||||||
|
{ ok: true, text: "Обе формы: фон #d4f6f8" },
|
||||||
|
{ ok: true, text: "Чекбокс согласия обязателен в каждой форме" },
|
||||||
|
{ ok: true, text: "Кнопки: bb-btn-primary (#FFA39C)" },
|
||||||
|
{ ok: true, text: "Все input/select: height 50px (bb-input, bb-select)" },
|
||||||
|
{ ok: false, text: "Не убирать чекбокс согласия на обработку данных" },
|
||||||
|
{ ok: false, text: "Не менять стиль кнопки на outline или teal" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</LlmBlock>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,173 +1,10 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
import ContactFormsPageClient from "./ContactFormsPageClient";
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
|
||||||
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
|
||||||
import { ContactFormsBlock } from "@/components/blocks/ContactFormsBlock";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Формы записи. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
title: "Формы записи. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
};
|
};
|
||||||
|
|
||||||
const LLM_FORMS_TEXT = `
|
|
||||||
БЛОК: Формы записи и обратной связи
|
|
||||||
Источник: perm.oclinica.ru/lor — два блока форм на одной странице
|
|
||||||
Версия: v1.2
|
|
||||||
|
|
||||||
ФОРМА 1: «Будьте здоровы!» (форма записи на приём)
|
|
||||||
Позиция: после блока отзывов
|
|
||||||
Заголовок H2: «Будьте здоровы!» — 36px, bold, #000000, line-height 38px
|
|
||||||
Подзаголовок: «Запишитесь на приём к врачу!»
|
|
||||||
Фон секции: #d4f6f8 (светло-бирюзовый)
|
|
||||||
Поля формы:
|
|
||||||
1. Текстовый input: placeholder «Введите ваше имя» (height 50px)
|
|
||||||
2. Телефонный input: placeholder «Введите ваш телефон» (height 50px)
|
|
||||||
3. Select: «Выберите ЛОР врача» (height 50px)
|
|
||||||
4. Checkbox: «Отправляя данные, я даю согласие на обработку персональных данных»
|
|
||||||
Кнопка: «Запишите меня!» — стиль bb-btn-primary (#FFA39C)
|
|
||||||
Ширина формы: ~400px центрирована или в колонку
|
|
||||||
|
|
||||||
ФОРМА 2: «Узнайте стоимость операции» (консультация)
|
|
||||||
Позиция: после блока новостей
|
|
||||||
Заголовок H2: «Узнайте стоимость операции» — 36px, bold, #000000, line-height 38px
|
|
||||||
Подзаголовок: «Проконсультируйтесь с ассистентом хирурга»
|
|
||||||
Фон секции: #d4f6f8 (тот же что и форма 1)
|
|
||||||
Поля формы:
|
|
||||||
1. Текстовый input: placeholder «каша» / «Введите ваше имя» (height 50px)
|
|
||||||
2. Телефонный input: placeholder «Введите ваш телефон» (height 50px)
|
|
||||||
3. Checkbox: «Отправляя данные, я даю согласие на обработку персональных данных»
|
|
||||||
Кнопка: «Перезвоните мне» — стиль bb-btn-primary (#FFA39C)
|
|
||||||
|
|
||||||
ОБЩИЕ ПРАВИЛА:
|
|
||||||
— Оба поля input/select: bb-input / bb-select (height 50px, border 1px solid #ccc)
|
|
||||||
— Чекбокс обязателен в обеих формах
|
|
||||||
— Кнопка отправки: всегда bb-btn-primary (#FFA39C)
|
|
||||||
— Обе формы на бирюзовом фоне (#d4f6f8)
|
|
||||||
|
|
||||||
ПРАВИЛА:
|
|
||||||
✓ Чекбокс согласия обязателен в каждой форме
|
|
||||||
✓ Кнопки отправки: bb-btn-primary (#FFA39C)
|
|
||||||
✓ Обе формы на фоне #d4f6f8
|
|
||||||
✕ Не менять порядок полей
|
|
||||||
✕ Не убирать чекбокс согласия
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const CHANGELOG: ChangelogEntry[] = [
|
|
||||||
{
|
|
||||||
version: "v1.2",
|
|
||||||
date: "24.03.2026",
|
|
||||||
changes: [
|
|
||||||
"Кнопка: bb-btn-md заменена на bb-btn-lg (18px, bold, как на реальном сайте)",
|
|
||||||
"border-radius кнопки: 7px исправлен на 4px (исправлено в globals.css)",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
version: "v1.1",
|
|
||||||
date: "24.03.2026",
|
|
||||||
changes: [
|
|
||||||
"H2: размер на 36px, цвет на #000000, line-height 38px",
|
|
||||||
"Фон формы 1: с #b8e6ed на #d4f6f8",
|
|
||||||
"Фон формы 2: с #ffffff на #d4f6f8 (обе формы на одном фоне)",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function ContactFormsPage() {
|
export default function ContactFormsPage() {
|
||||||
return (
|
return <ContactFormsPageClient />;
|
||||||
<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)" }}>
|
|
||||||
Формы записи
|
|
||||||
</h1>
|
|
||||||
<BlockMetaBar path="/blocks/contact-forms" defaultVersion="v1.2" defaultIsInPreview={false} defaultChangelog={CHANGELOG} />
|
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
Два блока форм с perm.oclinica.ru/lor — запись на приём и запрос стоимости операции.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Живой пример */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Живой пример
|
|
||||||
</h2>
|
|
||||||
<ContactFormsBlock />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Сравнение двух форм */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Сравнение двух форм
|
|
||||||
</h2>
|
|
||||||
<div className="overflow-x-auto rounded-xl" style={{ border: "1px solid var(--bb-border)" }}>
|
|
||||||
<table className="w-full text-sm border-collapse">
|
|
||||||
<thead>
|
|
||||||
<tr style={{ background: "var(--bb-sidebar-bg)" }}>
|
|
||||||
<th className="text-left px-4 py-3 text-xs font-semibold" style={{ color: "var(--bb-text-muted)", borderBottom: "1px solid var(--bb-border)" }}>Параметр</th>
|
|
||||||
<th className="text-left px-4 py-3 text-xs font-semibold" style={{ color: "var(--bb-text-muted)", borderBottom: "1px solid var(--bb-border)" }}>Форма 1 «Будьте здоровы!»</th>
|
|
||||||
<th className="text-left px-4 py-3 text-xs font-semibold" style={{ color: "var(--bb-text-muted)", borderBottom: "1px solid var(--bb-border)" }}>Форма 2 «Стоимость операции»</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{[
|
|
||||||
["Заголовок", "«Будьте здоровы!»", "«Узнайте стоимость операции»"],
|
|
||||||
["Подзаголовок", "«Запишитесь на приём к врачу!»", "«Проконсультируйтесь с ассистентом хирурга»"],
|
|
||||||
["Фон секции", "#d4f6f8", "#d4f6f8"],
|
|
||||||
["Поля", "Имя + Телефон + Select врача + Checkbox", "Имя + Телефон + Checkbox"],
|
|
||||||
["Кнопка", "«Запишите меня!»", "«Перезвоните мне»"],
|
|
||||||
["Стиль кнопки", "bb-btn-primary (#FFA39C)", "bb-btn-primary (#FFA39C)"],
|
|
||||||
].map(([param, f1, f2]) => (
|
|
||||||
<tr key={param} style={{ borderTop: "1px solid var(--bb-border)" }}>
|
|
||||||
<td className="px-4 py-2 font-medium text-xs" style={{ color: "var(--bb-text)" }}>{param}</td>
|
|
||||||
<td className="px-4 py-2 text-xs" style={{ color: "var(--bb-text-muted)" }}>{f1}</td>
|
|
||||||
<td className="px-4 py-2 text-xs" style={{ color: "var(--bb-text-muted)" }}>{f2}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* LLM блок */}
|
|
||||||
<LlmBlock path="/blocks/contact-forms" version="v1.2" specText={LLM_FORMS_TEXT}>
|
|
||||||
<LlmSection title="Форма 1 — Запись на приём" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Поле", "Тип", "Placeholder"]}
|
|
||||||
rows={[
|
|
||||||
["Имя", "input[type=text]", "«Введите ваше имя»"],
|
|
||||||
["Телефон", "input[type=tel]", "«Введите ваш телефон»"],
|
|
||||||
["Врач", "select", "«Выберите ЛОР врача»"],
|
|
||||||
["Согласие", "checkbox", "«Отправляя данные, я даю согласие...»"],
|
|
||||||
["Кнопка", "button bb-btn-primary", "«Запишите меня!»"],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Форма 2 — Запрос стоимости" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Поле", "Тип", "Placeholder"]}
|
|
||||||
rows={[
|
|
||||||
["Имя", "input[type=text]", "«Введите ваше имя»"],
|
|
||||||
["Телефон", "input[type=tel]", "«Введите ваш телефон»"],
|
|
||||||
["Согласие", "checkbox", "«Отправляя данные, я даю согласие...»"],
|
|
||||||
["Кнопка", "button bb-btn-primary", "«Перезвоните мне»"],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Правила" />
|
|
||||||
<LlmRules
|
|
||||||
rules={[
|
|
||||||
{ ok: true, text: "Обе формы: фон #d4f6f8" },
|
|
||||||
{ ok: true, text: "Чекбокс согласия обязателен в каждой форме" },
|
|
||||||
{ ok: true, text: "Кнопки: bb-btn-primary (#FFA39C)" },
|
|
||||||
{ ok: true, text: "Все input/select: height 50px (bb-input, bb-select)" },
|
|
||||||
{ ok: false, text: "Не убирать чекбокс согласия на обработку данных" },
|
|
||||||
{ ok: false, text: "Не менять стиль кнопки на outline или teal" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</LlmBlock>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||||
|
import { BlockMetaBar, type SnapshotData } from "@/components/ui/BlockMetaBar";
|
||||||
|
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
||||||
|
import { FooterBlock } from "@/components/blocks/FooterBlock";
|
||||||
|
|
||||||
|
const BLOCK_PATH = "/blocks/contact";
|
||||||
|
|
||||||
|
const FOOTER_COLUMNS = [
|
||||||
|
{
|
||||||
|
title: "О клинике",
|
||||||
|
links: ["Лицензия", "Миссия", "Врачи", "Вакансии", "История", "Образовательная деятельность", "При инфо"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Заболевания",
|
||||||
|
links: ["Ринит", "Отит", "Гайморит", "Тонзиллит", "Полипы носа", "Искривление перегородки"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Вопрос-ответ",
|
||||||
|
links: [
|
||||||
|
"Что нужно знать до операции на ухо",
|
||||||
|
"Что нужно знать до операции на нос",
|
||||||
|
"Отзывы до и после лечения у детей",
|
||||||
|
"Что нужно знать при лечении у детей",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Операции",
|
||||||
|
links: ["Септопластика", "Турбинопластика", "Тонзиллэктомия", "Аденотомия", "Тимпанопластика", "Мирингопластика"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const LLM_FOOTER_TEXT = `
|
||||||
|
БЛОК: Подвал сайта (Footer)
|
||||||
|
Источник: perm.oclinica.ru/lor — нижняя часть страницы
|
||||||
|
Версия: v1.1
|
||||||
|
|
||||||
|
СТРУКТУРА ПОДВАЛА:
|
||||||
|
|
||||||
|
1. ОСНОВНАЯ ЧАСТЬ — 4 колонки ссылок:
|
||||||
|
Колонка 1 «О клинике»: Лицензия, Миссия, Врачи, Вакансии, История, Образовательная деятельность
|
||||||
|
Колонка 2 «Заболевания»: Ринит, Отит, Гайморит, Тонзиллит, Полипы носа, Искривление перегородки
|
||||||
|
Колонка 3 «Вопрос-ответ»: 4 вопроса об операциях и лечении
|
||||||
|
Колонка 4 «Операции»: Септопластика, Турбинопластика, Тонзиллэктомия, Аденотомия и др.
|
||||||
|
|
||||||
|
2. НИЖНЯЯ ПОЛОСА:
|
||||||
|
Левая: Логотип «КЛИНИКА УХО ГОРЛО НОС ИМ. ПРОФ. Е.Н.ОЛЕНЕВОЙ»
|
||||||
|
Центр:
|
||||||
|
— «Мы находимся по адресу: Пермь, ул. Г. Звезда...»
|
||||||
|
— Иконки соцсетей: VK, OK, YouTube, Telegram и другие
|
||||||
|
Правая: Часы работы:
|
||||||
|
— Пн-пт: 9:00–21:00
|
||||||
|
— Сб: 9:00–18:00
|
||||||
|
— Вс: выходной
|
||||||
|
— Вторая клиника часы отдельно
|
||||||
|
|
||||||
|
ЦВЕТА:
|
||||||
|
Фон подвала: #fff или светло-серый (#f8f9fa)
|
||||||
|
Заголовки колонок: #111827, font-weight 600
|
||||||
|
Ссылки: #53514e (--brand-073m), hover: #0089c3
|
||||||
|
Разделитель: border-top 1px solid #e5e7eb
|
||||||
|
Часы работы: #374151
|
||||||
|
|
||||||
|
ПРАВИЛА:
|
||||||
|
✓ 4 колонки ссылок в основной части
|
||||||
|
✓ Логотип в нижней полосе слева
|
||||||
|
✓ Адрес + соцсети в центре нижней полосы
|
||||||
|
✓ Часы работы справа
|
||||||
|
✕ Не менять структуру 4 колонок
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
version: "v1.1",
|
||||||
|
date: "24.03.2026",
|
||||||
|
changes: [
|
||||||
|
"Колонка «О клинике»: обновлены все ссылки по реальному сайту (13 ссылок)",
|
||||||
|
"Колонка «Заболевания»: обновлены ссылки (5 категорий)",
|
||||||
|
"Колонка «Вопрос-ответ»: обновлены ссылки (6 пунктов)",
|
||||||
|
"Колонка «Операции»: обновлены ссылки (11 операций, было 6)",
|
||||||
|
"Два адреса: Клары Цеткин, 9 + Газеты Звезда, 31А",
|
||||||
|
"Два графика работы по филиалам",
|
||||||
|
"Соцсети: добавлен Дзен",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ContactPageClient() {
|
||||||
|
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)" }}>
|
||||||
|
Подвал (Footer)
|
||||||
|
</h1>
|
||||||
|
<BlockMetaBar
|
||||||
|
path={BLOCK_PATH}
|
||||||
|
defaultVersion="v1.1"
|
||||||
|
defaultIsInPreview={false}
|
||||||
|
defaultChangelog={CHANGELOG}
|
||||||
|
onSnapshotSelect={setSnapshot}
|
||||||
|
/>
|
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
Подвал сайта с perm.oclinica.ru — 4 колонки ссылок, логотип, адрес, часы работы, соцсети.
|
||||||
|
</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}>
|
||||||
|
<FooterBlock />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Колонки */}
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||||
|
Четыре колонки ссылок
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-4 gap-3">
|
||||||
|
{FOOTER_COLUMNS.map((col) => (
|
||||||
|
<div
|
||||||
|
key={col.title}
|
||||||
|
className="rounded-xl p-3 space-y-1"
|
||||||
|
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}
|
||||||
|
>
|
||||||
|
<p className="font-semibold text-xs" style={{ color: "var(--bb-text)" }}>
|
||||||
|
{col.title}
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px]" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
{col.links.length} ссылок
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* LLM блок */}
|
||||||
|
<LlmBlock path="/blocks/contact" version="v1.1" specText={LLM_FOOTER_TEXT}>
|
||||||
|
<LlmSection title="Структура подвала" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Зона", "Содержимое", "Фон"]}
|
||||||
|
rows={[
|
||||||
|
["4 колонки ссылок", "О клинике / Заболевания / Вопрос-ответ / Операции", "#f8f9fa"],
|
||||||
|
["Нижняя полоса — лево", "Логотип клиники (иконка + текст)", "#fff"],
|
||||||
|
["Нижняя полоса — центр", "Адрес + иконки соцсетей (VK, OK, YT, TG)", "#fff"],
|
||||||
|
["Нижняя полоса — право", "Часы работы Пн–пт / Сб / Вс", "#fff"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Часы работы" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["День", "Часы"]}
|
||||||
|
rows={[
|
||||||
|
["Пн–пт", "9:00–21:00"],
|
||||||
|
["Сб", "9:00–18:00"],
|
||||||
|
["Вс", "выходной"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Цвета" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Элемент", "Цвет", "Токен"]}
|
||||||
|
rows={[
|
||||||
|
["Заголовки колонок", "#111827, font-weight 600", "—"],
|
||||||
|
["Ссылки (default)", "#53514e", "--brand-073m"],
|
||||||
|
["Ссылки (hover)", "#0089c3", "--brand-053m"],
|
||||||
|
["Фон основной части", "#f8f9fa", "—"],
|
||||||
|
["Фон нижней полосы", "#ffffff", "—"],
|
||||||
|
["Разделитель", "1px solid #e5e7eb", "—"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Правила" />
|
||||||
|
<LlmRules
|
||||||
|
rules={[
|
||||||
|
{ ok: true, text: "4 колонки: О клинике / Заболевания / Вопрос-ответ / Операции" },
|
||||||
|
{ ok: true, text: "Логотип в нижней полосе слева" },
|
||||||
|
{ ok: true, text: "Адрес и соцсети в центре" },
|
||||||
|
{ ok: true, text: "Часы работы справа" },
|
||||||
|
{ ok: false, text: "Не менять структуру и порядок 4 колонок" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</LlmBlock>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,187 +1,10 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
import ContactPageClient from "./ContactPageClient";
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
|
||||||
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
|
||||||
import { FooterBlock } from "@/components/blocks/FooterBlock";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Подвал (Footer). Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
title: "Подвал (Footer). Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
};
|
};
|
||||||
|
|
||||||
const FOOTER_COLUMNS = [
|
|
||||||
{
|
|
||||||
title: "О клинике",
|
|
||||||
links: ["Лицензия", "Миссия", "Врачи", "Вакансии", "История", "Образовательная деятельность", "При инфо"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Заболевания",
|
|
||||||
links: ["Ринит", "Отит", "Гайморит", "Тонзиллит", "Полипы носа", "Искривление перегородки"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Вопрос-ответ",
|
|
||||||
links: [
|
|
||||||
"Что нужно знать до операции на ухо",
|
|
||||||
"Что нужно знать до операции на нос",
|
|
||||||
"Отзывы до и после лечения у детей",
|
|
||||||
"Что нужно знать при лечении у детей",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Операции",
|
|
||||||
links: ["Септопластика", "Турбинопластика", "Тонзиллэктомия", "Аденотомия", "Тимпанопластика", "Мирингопластика"],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const LLM_FOOTER_TEXT = `
|
|
||||||
БЛОК: Подвал сайта (Footer)
|
|
||||||
Источник: perm.oclinica.ru/lor — нижняя часть страницы
|
|
||||||
Версия: v1.1
|
|
||||||
|
|
||||||
СТРУКТУРА ПОДВАЛА:
|
|
||||||
|
|
||||||
1. ОСНОВНАЯ ЧАСТЬ — 4 колонки ссылок:
|
|
||||||
Колонка 1 «О клинике»: Лицензия, Миссия, Врачи, Вакансии, История, Образовательная деятельность
|
|
||||||
Колонка 2 «Заболевания»: Ринит, Отит, Гайморит, Тонзиллит, Полипы носа, Искривление перегородки
|
|
||||||
Колонка 3 «Вопрос-ответ»: 4 вопроса об операциях и лечении
|
|
||||||
Колонка 4 «Операции»: Септопластика, Турбинопластика, Тонзиллэктомия, Аденотомия и др.
|
|
||||||
|
|
||||||
2. НИЖНЯЯ ПОЛОСА:
|
|
||||||
Левая: Логотип «КЛИНИКА УХО ГОРЛО НОС ИМ. ПРОФ. Е.Н.ОЛЕНЕВОЙ»
|
|
||||||
Центр:
|
|
||||||
— «Мы находимся по адресу: Пермь, ул. Г. Звезда...»
|
|
||||||
— Иконки соцсетей: VK, OK, YouTube, Telegram и другие
|
|
||||||
Правая: Часы работы:
|
|
||||||
— Пн-пт: 9:00–21:00
|
|
||||||
— Сб: 9:00–18:00
|
|
||||||
— Вс: выходной
|
|
||||||
— Вторая клиника часы отдельно
|
|
||||||
|
|
||||||
ЦВЕТА:
|
|
||||||
Фон подвала: #fff или светло-серый (#f8f9fa)
|
|
||||||
Заголовки колонок: #111827, font-weight 600
|
|
||||||
Ссылки: #53514e (--brand-073m), hover: #0089c3
|
|
||||||
Разделитель: border-top 1px solid #e5e7eb
|
|
||||||
Часы работы: #374151
|
|
||||||
|
|
||||||
ПРАВИЛА:
|
|
||||||
✓ 4 колонки ссылок в основной части
|
|
||||||
✓ Логотип в нижней полосе слева
|
|
||||||
✓ Адрес + соцсети в центре нижней полосы
|
|
||||||
✓ Часы работы справа
|
|
||||||
✕ Не менять структуру 4 колонок
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const CHANGELOG: ChangelogEntry[] = [
|
|
||||||
{
|
|
||||||
version: "v1.1",
|
|
||||||
date: "24.03.2026",
|
|
||||||
changes: [
|
|
||||||
"Колонка «О клинике»: обновлены все ссылки по реальному сайту (13 ссылок)",
|
|
||||||
"Колонка «Заболевания»: обновлены ссылки (5 категорий)",
|
|
||||||
"Колонка «Вопрос-ответ»: обновлены ссылки (6 пунктов)",
|
|
||||||
"Колонка «Операции»: обновлены ссылки (11 операций, было 6)",
|
|
||||||
"Два адреса: Клары Цеткин, 9 + Газеты Звезда, 31А",
|
|
||||||
"Два графика работы по филиалам",
|
|
||||||
"Соцсети: добавлен Дзен",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function ContactFooterPage() {
|
export default function ContactFooterPage() {
|
||||||
return (
|
return <ContactPageClient />;
|
||||||
<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)" }}>
|
|
||||||
Подвал (Footer)
|
|
||||||
</h1>
|
|
||||||
<BlockMetaBar path="/blocks/contact" defaultVersion="v1.1" defaultIsInPreview={false} defaultChangelog={CHANGELOG} />
|
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
Подвал сайта с perm.oclinica.ru — 4 колонки ссылок, логотип, адрес, часы работы, соцсети.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Живой пример */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Живой пример
|
|
||||||
</h2>
|
|
||||||
<FooterBlock />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Колонки */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Четыре колонки ссылок
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-4 gap-3">
|
|
||||||
{FOOTER_COLUMNS.map((col) => (
|
|
||||||
<div
|
|
||||||
key={col.title}
|
|
||||||
className="rounded-xl p-3 space-y-1"
|
|
||||||
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}
|
|
||||||
>
|
|
||||||
<p className="font-semibold text-xs" style={{ color: "var(--bb-text)" }}>
|
|
||||||
{col.title}
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px]" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
{col.links.length} ссылок
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* LLM блок */}
|
|
||||||
<LlmBlock path="/blocks/contact" version="v1.1" specText={LLM_FOOTER_TEXT}>
|
|
||||||
<LlmSection title="Структура подвала" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Зона", "Содержимое", "Фон"]}
|
|
||||||
rows={[
|
|
||||||
["4 колонки ссылок", "О клинике / Заболевания / Вопрос-ответ / Операции", "#f8f9fa"],
|
|
||||||
["Нижняя полоса — лево", "Логотип клиники (иконка + текст)", "#fff"],
|
|
||||||
["Нижняя полоса — центр", "Адрес + иконки соцсетей (VK, OK, YT, TG)", "#fff"],
|
|
||||||
["Нижняя полоса — право", "Часы работы Пн–пт / Сб / Вс", "#fff"],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Часы работы" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["День", "Часы"]}
|
|
||||||
rows={[
|
|
||||||
["Пн–пт", "9:00–21:00"],
|
|
||||||
["Сб", "9:00–18:00"],
|
|
||||||
["Вс", "выходной"],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Цвета" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Элемент", "Цвет", "Токен"]}
|
|
||||||
rows={[
|
|
||||||
["Заголовки колонок", "#111827, font-weight 600", "—"],
|
|
||||||
["Ссылки (default)", "#53514e", "--brand-073m"],
|
|
||||||
["Ссылки (hover)", "#0089c3", "--brand-053m"],
|
|
||||||
["Фон основной части", "#f8f9fa", "—"],
|
|
||||||
["Фон нижней полосы", "#ffffff", "—"],
|
|
||||||
["Разделитель", "1px solid #e5e7eb", "—"],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Правила" />
|
|
||||||
<LlmRules
|
|
||||||
rules={[
|
|
||||||
{ ok: true, text: "4 колонки: О клинике / Заболевания / Вопрос-ответ / Операции" },
|
|
||||||
{ ok: true, text: "Логотип в нижней полосе слева" },
|
|
||||||
{ ok: true, text: "Адрес и соцсети в центре" },
|
|
||||||
{ ok: true, text: "Часы работы справа" },
|
|
||||||
{ ok: false, text: "Не менять структуру и порядок 4 колонок" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</LlmBlock>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||||
|
import { DoctorsBlock, STATS, DOCTORS } from "@/components/blocks/DoctorsBlock";
|
||||||
|
import { BlockMetaBar, type SnapshotData } from "@/components/ui/BlockMetaBar";
|
||||||
|
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
||||||
|
|
||||||
|
const BLOCK_PATH = "/blocks/doctors";
|
||||||
|
|
||||||
|
const LLM_DOCTORS_TEXT = `
|
||||||
|
БЛОК: Наши врачи
|
||||||
|
Источник: perm.oclinica.ru/lor — блок под CEO-текстом
|
||||||
|
Версия: v1.2
|
||||||
|
|
||||||
|
СТРУКТУРА БЛОКА:
|
||||||
|
1. ЗАГОЛОВОК H2: «Приём ведут опытные ЛОР врачи»
|
||||||
|
Подзаголовок: описание принципа работы врачей клиники
|
||||||
|
Размер: 36px, font-bold, #000000, line-height: 38px
|
||||||
|
|
||||||
|
2. БЛОК СТАТИСТИКИ (3 показателя в ряд):
|
||||||
|
— «Ежедневно 27 ЛОР врачей работают в клинике»
|
||||||
|
— «В том числе 6 кандидатов медицинских наук»
|
||||||
|
— «Свыше 12 000 успешно проведённых операций»
|
||||||
|
Стиль: весь текст #60959c, bold, ~18px. Фона НЕТ. Снизу border-bottom 3px solid #60959c
|
||||||
|
|
||||||
|
3. СЕТКА КАРТОЧЕК ВРАЧЕЙ (6 штук, 6 в ряд):
|
||||||
|
Каждая карточка:
|
||||||
|
— Фото врача 110×150px, object-fit: cover, object-position: center top, border-radius 4px
|
||||||
|
— Имя (12px, font-weight 500, цвет #60959c)
|
||||||
|
— Специализация (11px, #374151)
|
||||||
|
Карточки без рамки, gap минимальный
|
||||||
|
|
||||||
|
ЦВЕТА:
|
||||||
|
Заголовок H2: #000000, 36px, line-height 38px
|
||||||
|
Статистика текст: #60959c (серо-бирюзовый)
|
||||||
|
Статистика черта: border-bottom 3px solid #60959c
|
||||||
|
Имя врача: #60959c
|
||||||
|
Специализация: #374151
|
||||||
|
|
||||||
|
ПРАВИЛА:
|
||||||
|
✓ Заголовок H2 + описание обязательны
|
||||||
|
✓ 3 stat-блока в ряд, без фоновых блоков
|
||||||
|
✓ Сетка 6 колонок (6 карточек в ряд)
|
||||||
|
✕ Не отображать более 6 врачей в основном блоке
|
||||||
|
✕ Не убирать статистику
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
version: "v1.2",
|
||||||
|
date: "24.03.2026",
|
||||||
|
changes: [
|
||||||
|
"H2: размер с ~30px на 36px, цвет с #111827 на #000000",
|
||||||
|
"H2 line-height: 38px",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "v1.1",
|
||||||
|
date: "23.03.2026",
|
||||||
|
changes: [
|
||||||
|
"6 реальных фото врачей с сайта",
|
||||||
|
"Статистика без фона, только border-bottom #60959c",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function DoctorsPageClient() {
|
||||||
|
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)" }}>
|
||||||
|
Блок «Наши врачи»
|
||||||
|
</h1>
|
||||||
|
<BlockMetaBar
|
||||||
|
path={BLOCK_PATH}
|
||||||
|
defaultVersion="v1.2"
|
||||||
|
defaultIsInPreview={true}
|
||||||
|
defaultChangelog={CHANGELOG}
|
||||||
|
onSnapshotSelect={setSnapshot}
|
||||||
|
/>
|
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
Блок на странице perm.oclinica.ru/lor — заголовок, 3 стат-блока, сетка из 6 карточек врачей.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Живой пример */}
|
||||||
|
<section className="space-y-4">
|
||||||
|
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||||
|
Живой пример
|
||||||
|
</h2>
|
||||||
|
{snapshot ? (
|
||||||
|
<div
|
||||||
|
className="rounded-xl p-8"
|
||||||
|
style={{ background: "#fff", border: "1px solid var(--bb-border)" }}
|
||||||
|
>
|
||||||
|
<style dangerouslySetInnerHTML={{ __html: snapshot.css }} />
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: snapshot.html }} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
data-block-capture={BLOCK_PATH}
|
||||||
|
className="rounded-xl p-8"
|
||||||
|
style={{ background: "#fff", border: "1px solid var(--bb-border)" }}
|
||||||
|
>
|
||||||
|
<DoctorsBlock />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Стат-блоки — разбор */}
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||||
|
Блок статистики
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-3 gap-6">
|
||||||
|
{STATS.map((s) => (
|
||||||
|
<div
|
||||||
|
key={s.num}
|
||||||
|
className="pb-3 space-y-1"
|
||||||
|
style={{ borderBottom: "3px solid #60959c" }}
|
||||||
|
>
|
||||||
|
<p className="text-base font-bold leading-snug" style={{ color: "#60959c" }}>
|
||||||
|
{s.prefix} {s.num} {s.label}
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] font-mono" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
цвет текста: #60959c · черта: 3px solid #60959c
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* LLM блок */}
|
||||||
|
<LlmBlock path="/blocks/doctors" version="v1.2" specText={LLM_DOCTORS_TEXT}>
|
||||||
|
<LlmSection title="Структура блока" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Элемент", "Содержимое", "Стиль"]}
|
||||||
|
rows={[
|
||||||
|
["H2", "«Приём ведут опытные ЛОР врачи»", "36px, font-bold, #000000, line-height 38px"],
|
||||||
|
["Подзаголовок", "Описание принципа работы", "14px, #374151, line-height 1.7"],
|
||||||
|
["Статистика", "3 блока в ряд, без фона", "#60959c bold + border-bottom 3px solid #60959c"],
|
||||||
|
["Сетка врачей", "6 карточек в 1 ряд", "фото 110×150px + имя #60959c + специализация #374151"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Три показателя" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Число", "Описание"]}
|
||||||
|
rows={STATS.map((s) => [`${s.prefix} ${s.num}`, s.label])}
|
||||||
|
/>
|
||||||
|
<LlmSection title="6 врачей слайдера (perm.oclinica.ru/lor)" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Имя", "Специализация"]}
|
||||||
|
rows={DOCTORS.map((d) => [d.name, d.spec])}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Карточка врача" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Поле", "Размер / Стиль"]}
|
||||||
|
rows={[
|
||||||
|
["Фото", "110×150px, object-fit: cover, object-position: center top, border-radius 4px"],
|
||||||
|
["Имя", "12px, font-weight 500, #60959c"],
|
||||||
|
["Специализация", "11px, #374151"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Правила" />
|
||||||
|
<LlmRules
|
||||||
|
rules={[
|
||||||
|
{ ok: true, text: "H2 + описание обязательны" },
|
||||||
|
{ ok: true, text: "3 stat-блока в ряд, без фоновых рамок" },
|
||||||
|
{ ok: true, text: "Сетка 6 карточек в 1 ряд (6 колонок)" },
|
||||||
|
{ ok: false, text: "Не показывать более 6 врачей в основном блоке" },
|
||||||
|
{ ok: false, text: "Не убирать статистику" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</LlmBlock>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,168 +1,10 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
import DoctorsPageClient from "./DoctorsPageClient";
|
||||||
import { DoctorsBlock, STATS, DOCTORS } from "@/components/blocks/DoctorsBlock";
|
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
|
||||||
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Блок «Наши врачи». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
title: "Блок «Наши врачи». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
};
|
};
|
||||||
|
|
||||||
const LLM_DOCTORS_TEXT = `
|
|
||||||
БЛОК: Наши врачи
|
|
||||||
Источник: perm.oclinica.ru/lor — блок под CEO-текстом
|
|
||||||
Версия: v1.2
|
|
||||||
|
|
||||||
СТРУКТУРА БЛОКА:
|
|
||||||
1. ЗАГОЛОВОК H2: «Приём ведут опытные ЛОР врачи»
|
|
||||||
Подзаголовок: описание принципа работы врачей клиники
|
|
||||||
Размер: 36px, font-bold, #000000, line-height: 38px
|
|
||||||
|
|
||||||
2. БЛОК СТАТИСТИКИ (3 показателя в ряд):
|
|
||||||
— «Ежедневно 27 ЛОР врачей работают в клинике»
|
|
||||||
— «В том числе 6 кандидатов медицинских наук»
|
|
||||||
— «Свыше 12 000 успешно проведённых операций»
|
|
||||||
Стиль: весь текст #60959c, bold, ~18px. Фона НЕТ. Снизу border-bottom 3px solid #60959c
|
|
||||||
|
|
||||||
3. СЕТКА КАРТОЧЕК ВРАЧЕЙ (6 штук, 6 в ряд):
|
|
||||||
Каждая карточка:
|
|
||||||
— Фото врача 110×150px, object-fit: cover, object-position: center top, border-radius 4px
|
|
||||||
— Имя (12px, font-weight 500, цвет #60959c)
|
|
||||||
— Специализация (11px, #374151)
|
|
||||||
Карточки без рамки, gap минимальный
|
|
||||||
|
|
||||||
ЦВЕТА:
|
|
||||||
Заголовок H2: #000000, 36px, line-height 38px
|
|
||||||
Статистика текст: #60959c (серо-бирюзовый)
|
|
||||||
Статистика черта: border-bottom 3px solid #60959c
|
|
||||||
Имя врача: #60959c
|
|
||||||
Специализация: #374151
|
|
||||||
|
|
||||||
ПРАВИЛА:
|
|
||||||
✓ Заголовок H2 + описание обязательны
|
|
||||||
✓ 3 stat-блока в ряд, без фоновых блоков
|
|
||||||
✓ Сетка 6 колонок (6 карточек в ряд)
|
|
||||||
✕ Не отображать более 6 врачей в основном блоке
|
|
||||||
✕ Не убирать статистику
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const CHANGELOG: ChangelogEntry[] = [
|
|
||||||
{
|
|
||||||
version: "v1.2",
|
|
||||||
date: "24.03.2026",
|
|
||||||
changes: [
|
|
||||||
"H2: размер с ~30px на 36px, цвет с #111827 на #000000",
|
|
||||||
"H2 line-height: 38px",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
version: "v1.1",
|
|
||||||
date: "23.03.2026",
|
|
||||||
changes: [
|
|
||||||
"6 реальных фото врачей с сайта",
|
|
||||||
"Статистика без фона, только border-bottom #60959c",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function DoctorsBlockPage() {
|
export default function DoctorsBlockPage() {
|
||||||
return (
|
return <DoctorsPageClient />;
|
||||||
<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)" }}>
|
|
||||||
Блок «Наши врачи»
|
|
||||||
</h1>
|
|
||||||
<BlockMetaBar path="/blocks/doctors" defaultVersion="v1.2" defaultIsInPreview={true} defaultChangelog={CHANGELOG} />
|
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
Блок на странице perm.oclinica.ru/lor — заголовок, 3 стат-блока, сетка из 6 карточек врачей.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Живой пример */}
|
|
||||||
<section className="space-y-4">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Живой пример
|
|
||||||
</h2>
|
|
||||||
<div
|
|
||||||
className="rounded-xl p-8"
|
|
||||||
style={{ background: "#fff", border: "1px solid var(--bb-border)" }}
|
|
||||||
>
|
|
||||||
<DoctorsBlock />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Стат-блоки — разбор */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Блок статистики
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-3 gap-6">
|
|
||||||
{STATS.map((s) => (
|
|
||||||
<div
|
|
||||||
key={s.num}
|
|
||||||
className="pb-3 space-y-1"
|
|
||||||
style={{ borderBottom: "3px solid #60959c" }}
|
|
||||||
>
|
|
||||||
<p className="text-base font-bold leading-snug" style={{ color: "#60959c" }}>
|
|
||||||
{s.prefix} {s.num} {s.label}
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px] font-mono" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
цвет текста: #60959c · черта: 3px solid #60959c
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* LLM блок */}
|
|
||||||
<LlmBlock path="/blocks/doctors" version="v1.2" specText={LLM_DOCTORS_TEXT}>
|
|
||||||
<LlmSection title="Структура блока" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Элемент", "Содержимое", "Стиль"]}
|
|
||||||
rows={[
|
|
||||||
["H2", "«Приём ведут опытные ЛОР врачи»", "36px, font-bold, #000000, line-height 38px"],
|
|
||||||
["Подзаголовок", "Описание принципа работы", "14px, #374151, line-height 1.7"],
|
|
||||||
["Статистика", "3 блока в ряд, без фона", "#60959c bold + border-bottom 3px solid #60959c"],
|
|
||||||
["Сетка врачей", "6 карточек в 1 ряд", "фото 110×150px + имя #60959c + специализация #374151"],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Три показателя" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Число", "Описание"]}
|
|
||||||
rows={STATS.map((s) => [`${s.prefix} ${s.num}`, s.label])}
|
|
||||||
/>
|
|
||||||
<LlmSection title="6 врачей слайдера (perm.oclinica.ru/lor)" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Имя", "Специализация"]}
|
|
||||||
rows={DOCTORS.map((d) => [d.name, d.spec])}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Карточка врача" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Поле", "Размер / Стиль"]}
|
|
||||||
rows={[
|
|
||||||
["Фото", "110×150px, object-fit: cover, object-position: center top, border-radius 4px"],
|
|
||||||
["Имя", "12px, font-weight 500, #60959c"],
|
|
||||||
["Специализация", "11px, #374151"],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Правила" />
|
|
||||||
<LlmRules
|
|
||||||
rules={[
|
|
||||||
{ ok: true, text: "H2 + описание обязательны" },
|
|
||||||
{ ok: true, text: "3 stat-блока в ряд, без фоновых рамок" },
|
|
||||||
{ ok: true, text: "Сетка 6 карточек в 1 ряд (6 колонок)" },
|
|
||||||
{ ok: false, text: "Не показывать более 6 врачей в основном блоке" },
|
|
||||||
{ ok: false, text: "Не убирать статистику" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</LlmBlock>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 type { Metadata } from "next";
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
import HeroPageClient from "./HeroPageClient";
|
||||||
import { HeroBlock, HERO_CHECKS } from "@/components/blocks/HeroBlock";
|
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
|
||||||
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Hero-баннер. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
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() {
|
export default function HeroPage() {
|
||||||
return (
|
return <HeroPageClient />;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,291 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||||
|
import { NavigationBlock } from "@/components/blocks/NavigationBlock";
|
||||||
|
import { NAV_ITEMS } from "@/components/blocks/navData";
|
||||||
|
import { BlockMetaBar, type SnapshotData } from "@/components/ui/BlockMetaBar";
|
||||||
|
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
||||||
|
|
||||||
|
const BLOCK_PATH = "/blocks/navigation";
|
||||||
|
|
||||||
|
const LLM_NAV_TEXT = `
|
||||||
|
БЛОК: Шапка и навигация сайта (Header)
|
||||||
|
Источник: perm.oclinica.ru/lor — реальная шапка сайта
|
||||||
|
Версия: v1.3
|
||||||
|
|
||||||
|
СТРУКТУРА ШАПКИ (2 зоны сверху вниз):
|
||||||
|
|
||||||
|
1. ВЕРХНЯЯ ПАНЕЛЬ (фон #fff, без рамок)
|
||||||
|
Три столбца:
|
||||||
|
— Столбец 1 (лево): Логотип клиники (logo-main.png, h-20)
|
||||||
|
— Столбец 2 (центр): Три ссылки вертикально с иконкой 📍:
|
||||||
|
· К. Цеткин, 9 (цвет #52b4bd, подчёркнутая)
|
||||||
|
· Клиника лечения кашля и аллергии (цвет #52b4bd, подчёркнутая)
|
||||||
|
· Центр диагностики и реабилитации (цвет #52b4bd, подчёркнутая)
|
||||||
|
— Столбец 3 (право): Телефон «/342/ 255-53-84» (25px, bold, #000) + кнопка «Заказать звонок» (bb-btn-md bb-btn-pill)
|
||||||
|
|
||||||
|
2. ГЛАВНОЕ МЕНЮ (~46px, фон #fff, border-top 1px solid #e5e7eb)
|
||||||
|
Пункты (8 штук): Клиника | ЛОР врачи | Заболевания | Вопрос-ответ | ЛОР операции | Сурдология | Цены | Контакты
|
||||||
|
Шрифт: 18px, weight 400
|
||||||
|
Цвет ссылок: #000
|
||||||
|
Активный / Hover: #0089c3
|
||||||
|
Разделители: border-right 1px solid #f3f4f6 между пунктами
|
||||||
|
|
||||||
|
ПРАВИЛА:
|
||||||
|
✓ Логотип всегда кликабелен — ведёт на главную страницу раздела
|
||||||
|
✓ Кнопка «Заказать звонок» всегда видна, pill-стиль
|
||||||
|
✓ Телефон кликабелен (tel: ссылка)
|
||||||
|
✓ Активный пункт меню — цвет #0089c3, остальные #000
|
||||||
|
✕ Не добавлять пункты меню, которых нет на сайте
|
||||||
|
✕ Не менять порядок пунктов меню
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
version: "v1.3",
|
||||||
|
date: "24.03.2026",
|
||||||
|
changes: [
|
||||||
|
"Подменю: выпадающие списки при hover для Клиника, Заболевания, Вопрос-ответ, ЛОР операции",
|
||||||
|
"Hover-эффект: бежевый фон #f5f0e6 при наведении на пункт меню",
|
||||||
|
"Пункты меню подчёркнуты (underline), без разделителей между ними",
|
||||||
|
"Убрана рамка между шапкой и меню (border-top)",
|
||||||
|
"Все пункты меню чёрного цвета #000 (было: первый #0089c3)",
|
||||||
|
"Уменьшен padding пунктов (px-2.5 вместо px-4) — все помещаются в ширину экрана",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "v1.2",
|
||||||
|
date: "24.03.2026",
|
||||||
|
changes: [
|
||||||
|
"Убрана рамка и тень вокруг шапки (как на реальном сайте)",
|
||||||
|
"Структура: 3 столбца (логотип | ссылки вертикально | телефон+кнопка) вместо горизонтального ряда",
|
||||||
|
"Логотип: реальный logo-main.png вместо кружка, размер h-20 (было h-12)",
|
||||||
|
"Кнопка: bb-btn-md (было bb-btn-sm)",
|
||||||
|
"Анатомия шапки: обновлена — 2 зоны вместо 3",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "v1.1",
|
||||||
|
date: "24.03.2026",
|
||||||
|
changes: [
|
||||||
|
"Адрес: «Б. Цитная, 3» заменён на «К. Цеткин, 9»",
|
||||||
|
"Ссылки: обновлены на «Клиника лечения кашля и аллергии» и «Центр диагностики и реабилитации»",
|
||||||
|
"Телефон: размер 25px (было ~14px)",
|
||||||
|
"Меню: font-size 18px (было 14px), цвет ссылок #000 (было #53514e)",
|
||||||
|
"Цвет ссылки адреса: #52b4bd (было #0089c3)",
|
||||||
|
"«ЛОР операции» без дефиса (было «ЛОР-операции»)",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function NavigationPageClient() {
|
||||||
|
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)" }}>
|
||||||
|
Шапка и навигация
|
||||||
|
</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. Три зоны: топ-бар, логотип, главное меню.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Живой пример */}
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||||
|
Живой пример
|
||||||
|
</h2>
|
||||||
|
{snapshot ? (
|
||||||
|
<div className="rounded-xl" style={{ paddingBottom: 40 }}>
|
||||||
|
<style dangerouslySetInnerHTML={{ __html: snapshot.css }} />
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: snapshot.html }} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div data-block-capture={BLOCK_PATH} className="rounded-xl" style={{ paddingBottom: 40 }}>
|
||||||
|
<NavigationBlock />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Анатомия */}
|
||||||
|
<section className="space-y-4">
|
||||||
|
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||||
|
Анатомия шапки
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
zone: "1. Верхняя панель",
|
||||||
|
bg: "#f0f9ff",
|
||||||
|
desc: "Три столбца: логотип (logo-main.png) | три ссылки вертикально с 📍 (К. Цеткин 9, Клиника кашля, Центр диагностики) | телефон 25px + кнопка «Заказать звонок» (pill)",
|
||||||
|
details: "Фон: #fff · Без рамок · Ссылки: #52b4bd, подчёркнутые",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
zone: "2. Главное меню",
|
||||||
|
bg: "#fefce8",
|
||||||
|
desc: "8 горизонтальных пунктов: Клиника | ЛОР врачи | Заболевания | Вопрос-ответ | ЛОР операции | Сурдология | Цены | Контакты",
|
||||||
|
details: "Фон: #fff · border-top · 18px · Активный: #0089c3, остальные: #000",
|
||||||
|
},
|
||||||
|
].map((z) => (
|
||||||
|
<div
|
||||||
|
key={z.zone}
|
||||||
|
className="rounded-xl p-4 space-y-2"
|
||||||
|
style={{ background: z.bg, border: "1px solid var(--bb-border)" }}
|
||||||
|
>
|
||||||
|
<p className="font-semibold text-sm" style={{ color: "var(--bb-text)" }}>
|
||||||
|
{z.zone}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
{z.desc}
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] font-mono mt-1" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
{z.details}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Пункты меню */}
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||||
|
Пункты главного меню
|
||||||
|
</h2>
|
||||||
|
<div className="rounded-xl border overflow-visible" style={{ borderColor: "var(--bb-border)", background: "#fff" }}>
|
||||||
|
<div className="flex items-center justify-center flex-wrap" style={{ borderTop: "1px solid #e5e7eb" }}>
|
||||||
|
{NAV_ITEMS.map((item) => (
|
||||||
|
<div key={item.label} className="bb-nav-item-wrap relative group">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="bb-nav-item px-2.5 py-3 whitespace-nowrap inline-flex items-center gap-1"
|
||||||
|
style={{
|
||||||
|
fontSize: 18,
|
||||||
|
color: "#000",
|
||||||
|
fontWeight: 400,
|
||||||
|
textDecoration: "underline",
|
||||||
|
textUnderlineOffset: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
{item.submenu.length > 0 && (
|
||||||
|
<span className="text-xs" style={{ color: "#9ca3af", textDecoration: "none", display: "inline-block" }}>▾</span>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
{item.submenu.length > 0 && (
|
||||||
|
<div
|
||||||
|
className="absolute left-0 top-full z-50 hidden group-hover:block min-w-[220px] py-1 rounded shadow-lg"
|
||||||
|
style={{ background: "#fff", border: "1px solid #e5e7eb" }}
|
||||||
|
>
|
||||||
|
{item.submenu.map((sub) => (
|
||||||
|
<a
|
||||||
|
key={sub}
|
||||||
|
href="#"
|
||||||
|
className="block px-4 py-2 text-sm bb-nav-sub-item"
|
||||||
|
style={{ color: "#333", textDecoration: "none" }}
|
||||||
|
>
|
||||||
|
{sub}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
Порядок фиксирован. Все пункты чёрные (#000), подчёркнуты. Hover — бежевый фон #f5f0e6. ▾ — выпадающее подменю при наведении.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Токены */}
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||||
|
Цвета и токены
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
|
{[
|
||||||
|
{ label: "Фон шапки", value: "#ffffff", token: "—" },
|
||||||
|
{ label: "Пункты меню", value: "#000000", token: "—" },
|
||||||
|
{ label: "Hover фон меню", value: "#f5f0e6", token: "bb-nav-item:hover" },
|
||||||
|
{ label: "Ссылки адресов", value: "#52b4bd", token: "—" },
|
||||||
|
{ label: "Кнопка «Заказать»", value: "#e9e4d4", token: "bb-btn-pill" },
|
||||||
|
{ label: "Подменю фон", value: "#ffffff", token: "—" },
|
||||||
|
{ label: "Подменю hover", value: "#f5f0e6", token: "bb-nav-sub-item:hover" },
|
||||||
|
{ label: "Телефон", value: "#000000", token: "25px, bold" },
|
||||||
|
].map((t) => (
|
||||||
|
<div
|
||||||
|
key={t.label}
|
||||||
|
className="p-3 rounded-xl space-y-2"
|
||||||
|
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-8 h-8 rounded border"
|
||||||
|
style={{ background: t.value, borderColor: "var(--bb-border)" }}
|
||||||
|
/>
|
||||||
|
<p className="text-xs font-medium" style={{ color: "var(--bb-text)" }}>
|
||||||
|
{t.label}
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] font-mono" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
{t.value}
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px]" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
{t.token}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* LLM блок */}
|
||||||
|
<LlmBlock path="/blocks/navigation" version="v1.3" specText={LLM_NAV_TEXT}>
|
||||||
|
<LlmSection title="Зоны шапки" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Зона", "Фон", "Содержимое"]}
|
||||||
|
rows={[
|
||||||
|
["Верхняя панель", "#fff, без рамок", "Логотип (logo-main.png) | 3 ссылки вертикально с 📍 (#52b4bd) | Телефон 25px + кнопка pill"],
|
||||||
|
["Главное меню", "#fff, border-top", "8 пунктов 18px: Клиника / ЛОР врачи / Заболевания / Вопрос-ответ / ЛОР операции / Сурдология / Цены / Контакты"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Цвета" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Элемент", "Цвет", "Токен"]}
|
||||||
|
rows={[
|
||||||
|
["Фон шапки", "#ffffff", "—"],
|
||||||
|
["Ссылки меню (default)", "#000000", "—"],
|
||||||
|
["Активный пункт / hover", "#0089c3", "--brand-053m"],
|
||||||
|
["Кнопка «Заказать звонок»", "#e9e4d4 / border #d5cfbd", "bb-btn-pill"],
|
||||||
|
["Телефон", "#111827, font-weight bold", "—"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Правила" />
|
||||||
|
<LlmRules
|
||||||
|
rules={[
|
||||||
|
{ ok: true, text: "Логотип ведёт на главную страницу раздела" },
|
||||||
|
{ ok: true, text: "Кнопка «Заказать звонок» всегда видна, pill-стиль" },
|
||||||
|
{ ok: true, text: "Телефон — кликабельная ссылка tel:" },
|
||||||
|
{ ok: true, text: "Активный пункт меню — цвет #0089c3" },
|
||||||
|
{ ok: false, text: "Не добавлять пункты меню, которых нет на сайте" },
|
||||||
|
{ ok: false, text: "Не менять порядок 8 пунктов меню" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</LlmBlock>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,276 +1,10 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
import NavigationPageClient from "./NavigationPageClient";
|
||||||
import { NavigationBlock } from "@/components/blocks/NavigationBlock";
|
|
||||||
import { NAV_ITEMS } from "@/components/blocks/navData";
|
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
|
||||||
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Шапка и навигация. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
title: "Шапка и навигация. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
};
|
};
|
||||||
|
|
||||||
const LLM_NAV_TEXT = `
|
|
||||||
БЛОК: Шапка и навигация сайта (Header)
|
|
||||||
Источник: perm.oclinica.ru/lor — реальная шапка сайта
|
|
||||||
Версия: v1.3
|
|
||||||
|
|
||||||
СТРУКТУРА ШАПКИ (2 зоны сверху вниз):
|
|
||||||
|
|
||||||
1. ВЕРХНЯЯ ПАНЕЛЬ (фон #fff, без рамок)
|
|
||||||
Три столбца:
|
|
||||||
— Столбец 1 (лево): Логотип клиники (logo-main.png, h-20)
|
|
||||||
— Столбец 2 (центр): Три ссылки вертикально с иконкой 📍:
|
|
||||||
· К. Цеткин, 9 (цвет #52b4bd, подчёркнутая)
|
|
||||||
· Клиника лечения кашля и аллергии (цвет #52b4bd, подчёркнутая)
|
|
||||||
· Центр диагностики и реабилитации (цвет #52b4bd, подчёркнутая)
|
|
||||||
— Столбец 3 (право): Телефон «/342/ 255-53-84» (25px, bold, #000) + кнопка «Заказать звонок» (bb-btn-md bb-btn-pill)
|
|
||||||
|
|
||||||
2. ГЛАВНОЕ МЕНЮ (~46px, фон #fff, border-top 1px solid #e5e7eb)
|
|
||||||
Пункты (8 штук): Клиника | ЛОР врачи | Заболевания | Вопрос-ответ | ЛОР операции | Сурдология | Цены | Контакты
|
|
||||||
Шрифт: 18px, weight 400
|
|
||||||
Цвет ссылок: #000
|
|
||||||
Активный / Hover: #0089c3
|
|
||||||
Разделители: border-right 1px solid #f3f4f6 между пунктами
|
|
||||||
|
|
||||||
ПРАВИЛА:
|
|
||||||
✓ Логотип всегда кликабелен — ведёт на главную страницу раздела
|
|
||||||
✓ Кнопка «Заказать звонок» всегда видна, pill-стиль
|
|
||||||
✓ Телефон кликабелен (tel: ссылка)
|
|
||||||
✓ Активный пункт меню — цвет #0089c3, остальные #000
|
|
||||||
✕ Не добавлять пункты меню, которых нет на сайте
|
|
||||||
✕ Не менять порядок пунктов меню
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const CHANGELOG: ChangelogEntry[] = [
|
|
||||||
{
|
|
||||||
version: "v1.3",
|
|
||||||
date: "24.03.2026",
|
|
||||||
changes: [
|
|
||||||
"Подменю: выпадающие списки при hover для Клиника, Заболевания, Вопрос-ответ, ЛОР операции",
|
|
||||||
"Hover-эффект: бежевый фон #f5f0e6 при наведении на пункт меню",
|
|
||||||
"Пункты меню подчёркнуты (underline), без разделителей между ними",
|
|
||||||
"Убрана рамка между шапкой и меню (border-top)",
|
|
||||||
"Все пункты меню чёрного цвета #000 (было: первый #0089c3)",
|
|
||||||
"Уменьшен padding пунктов (px-2.5 вместо px-4) — все помещаются в ширину экрана",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
version: "v1.2",
|
|
||||||
date: "24.03.2026",
|
|
||||||
changes: [
|
|
||||||
"Убрана рамка и тень вокруг шапки (как на реальном сайте)",
|
|
||||||
"Структура: 3 столбца (логотип | ссылки вертикально | телефон+кнопка) вместо горизонтального ряда",
|
|
||||||
"Логотип: реальный logo-main.png вместо кружка, размер h-20 (было h-12)",
|
|
||||||
"Кнопка: bb-btn-md (было bb-btn-sm)",
|
|
||||||
"Анатомия шапки: обновлена — 2 зоны вместо 3",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
version: "v1.1",
|
|
||||||
date: "24.03.2026",
|
|
||||||
changes: [
|
|
||||||
"Адрес: «Б. Цитная, 3» заменён на «К. Цеткин, 9»",
|
|
||||||
"Ссылки: обновлены на «Клиника лечения кашля и аллергии» и «Центр диагностики и реабилитации»",
|
|
||||||
"Телефон: размер 25px (было ~14px)",
|
|
||||||
"Меню: font-size 18px (было 14px), цвет ссылок #000 (было #53514e)",
|
|
||||||
"Цвет ссылки адреса: #52b4bd (было #0089c3)",
|
|
||||||
"«ЛОР операции» без дефиса (было «ЛОР-операции»)",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function NavigationPage() {
|
export default function NavigationPage() {
|
||||||
return (
|
return <NavigationPageClient />;
|
||||||
<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)" }}>
|
|
||||||
Шапка и навигация
|
|
||||||
</h1>
|
|
||||||
<BlockMetaBar path="/blocks/navigation" defaultVersion="v1.3" defaultIsInPreview={true} defaultChangelog={CHANGELOG} />
|
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
Точное воспроизведение шапки с perm.oclinica.ru/lor. Три зоны: топ-бар, логотип, главное меню.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Живой пример */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Живой пример
|
|
||||||
</h2>
|
|
||||||
<div className="rounded-xl" style={{ paddingBottom: 40 }}>
|
|
||||||
<NavigationBlock />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Анатомия */}
|
|
||||||
<section className="space-y-4">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Анатомия шапки
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{[
|
|
||||||
{
|
|
||||||
zone: "1. Верхняя панель",
|
|
||||||
bg: "#f0f9ff",
|
|
||||||
desc: "Три столбца: логотип (logo-main.png) | три ссылки вертикально с 📍 (К. Цеткин 9, Клиника кашля, Центр диагностики) | телефон 25px + кнопка «Заказать звонок» (pill)",
|
|
||||||
details: "Фон: #fff · Без рамок · Ссылки: #52b4bd, подчёркнутые",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
zone: "2. Главное меню",
|
|
||||||
bg: "#fefce8",
|
|
||||||
desc: "8 горизонтальных пунктов: Клиника | ЛОР врачи | Заболевания | Вопрос-ответ | ЛОР операции | Сурдология | Цены | Контакты",
|
|
||||||
details: "Фон: #fff · border-top · 18px · Активный: #0089c3, остальные: #000",
|
|
||||||
},
|
|
||||||
].map((z) => (
|
|
||||||
<div
|
|
||||||
key={z.zone}
|
|
||||||
className="rounded-xl p-4 space-y-2"
|
|
||||||
style={{ background: z.bg, border: "1px solid var(--bb-border)" }}
|
|
||||||
>
|
|
||||||
<p className="font-semibold text-sm" style={{ color: "var(--bb-text)" }}>
|
|
||||||
{z.zone}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
{z.desc}
|
|
||||||
</p>
|
|
||||||
<p className="text-[11px] font-mono mt-1" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
{z.details}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Пункты меню */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Пункты главного меню
|
|
||||||
</h2>
|
|
||||||
<div className="rounded-xl border overflow-visible" style={{ borderColor: "var(--bb-border)", background: "#fff" }}>
|
|
||||||
<div className="flex items-center justify-center flex-wrap" style={{ borderTop: "1px solid #e5e7eb" }}>
|
|
||||||
{NAV_ITEMS.map((item, i) => (
|
|
||||||
<div key={item.label} className="bb-nav-item-wrap relative group">
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="bb-nav-item px-2.5 py-3 whitespace-nowrap inline-flex items-center gap-1"
|
|
||||||
style={{
|
|
||||||
fontSize: 18,
|
|
||||||
color: "#000",
|
|
||||||
fontWeight: 400,
|
|
||||||
textDecoration: "underline",
|
|
||||||
textUnderlineOffset: "4px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
{item.submenu.length > 0 && (
|
|
||||||
<span className="text-xs" style={{ color: "#9ca3af", textDecoration: "none", display: "inline-block" }}>▾</span>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
{item.submenu.length > 0 && (
|
|
||||||
<div
|
|
||||||
className="absolute left-0 top-full z-50 hidden group-hover:block min-w-[220px] py-1 rounded shadow-lg"
|
|
||||||
style={{ background: "#fff", border: "1px solid #e5e7eb" }}
|
|
||||||
>
|
|
||||||
{item.submenu.map((sub) => (
|
|
||||||
<a
|
|
||||||
key={sub}
|
|
||||||
href="#"
|
|
||||||
className="block px-4 py-2 text-sm bb-nav-sub-item"
|
|
||||||
style={{ color: "#333", textDecoration: "none" }}
|
|
||||||
>
|
|
||||||
{sub}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
Порядок фиксирован. Все пункты чёрные (#000), подчёркнуты. Hover — бежевый фон #f5f0e6. ▾ — выпадающее подменю при наведении.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Токены */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Цвета и токены
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
||||||
{[
|
|
||||||
{ label: "Фон шапки", value: "#ffffff", token: "—" },
|
|
||||||
{ label: "Пункты меню", value: "#000000", token: "—" },
|
|
||||||
{ label: "Hover фон меню", value: "#f5f0e6", token: "bb-nav-item:hover" },
|
|
||||||
{ label: "Ссылки адресов", value: "#52b4bd", token: "—" },
|
|
||||||
{ label: "Кнопка «Заказать»", value: "#e9e4d4", token: "bb-btn-pill" },
|
|
||||||
{ label: "Подменю фон", value: "#ffffff", token: "—" },
|
|
||||||
{ label: "Подменю hover", value: "#f5f0e6", token: "bb-nav-sub-item:hover" },
|
|
||||||
{ label: "Телефон", value: "#000000", token: "25px, bold" },
|
|
||||||
].map((t) => (
|
|
||||||
<div
|
|
||||||
key={t.label}
|
|
||||||
className="p-3 rounded-xl space-y-2"
|
|
||||||
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="w-8 h-8 rounded border"
|
|
||||||
style={{ background: t.value, borderColor: "var(--bb-border)" }}
|
|
||||||
/>
|
|
||||||
<p className="text-xs font-medium" style={{ color: "var(--bb-text)" }}>
|
|
||||||
{t.label}
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px] font-mono" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
{t.value}
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px]" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
{t.token}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* LLM блок */}
|
|
||||||
<LlmBlock path="/blocks/navigation" version="v1.3" specText={LLM_NAV_TEXT}>
|
|
||||||
<LlmSection title="Зоны шапки" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Зона", "Фон", "Содержимое"]}
|
|
||||||
rows={[
|
|
||||||
["Верхняя панель", "#fff, без рамок", "Логотип (logo-main.png) | 3 ссылки вертикально с 📍 (#52b4bd) | Телефон 25px + кнопка pill"],
|
|
||||||
["Главное меню", "#fff, border-top", "8 пунктов 18px: Клиника / ЛОР врачи / Заболевания / Вопрос-ответ / ЛОР операции / Сурдология / Цены / Контакты"],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Цвета" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Элемент", "Цвет", "Токен"]}
|
|
||||||
rows={[
|
|
||||||
["Фон шапки", "#ffffff", "—"],
|
|
||||||
["Ссылки меню (default)", "#000000", "—"],
|
|
||||||
["Активный пункт / hover", "#0089c3", "--brand-053m"],
|
|
||||||
["Кнопка «Заказать звонок»", "#e9e4d4 / border #d5cfbd", "bb-btn-pill"],
|
|
||||||
["Телефон", "#111827, font-weight bold", "—"],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Правила" />
|
|
||||||
<LlmRules
|
|
||||||
rules={[
|
|
||||||
{ ok: true, text: "Логотип ведёт на главную страницу раздела" },
|
|
||||||
{ ok: true, text: "Кнопка «Заказать звонок» всегда видна, pill-стиль" },
|
|
||||||
{ ok: true, text: "Телефон — кликабельная ссылка tel:" },
|
|
||||||
{ ok: true, text: "Активный пункт меню — цвет #0089c3" },
|
|
||||||
{ ok: false, text: "Не добавлять пункты меню, которых нет на сайте" },
|
|
||||||
{ ok: false, text: "Не менять порядок 8 пунктов меню" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</LlmBlock>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||||
|
import { BlockMetaBar, type SnapshotData } from "@/components/ui/BlockMetaBar";
|
||||||
|
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
||||||
|
import { NewsBlock } from "@/components/blocks/NewsBlock";
|
||||||
|
|
||||||
|
const BLOCK_PATH = "/blocks/news";
|
||||||
|
|
||||||
|
const MOCK_NEWS = [
|
||||||
|
{
|
||||||
|
date: "20.12.2025",
|
||||||
|
title: "Наша работа клиники и новогодние праздники",
|
||||||
|
href: "#",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "11.08.2025",
|
||||||
|
title: "СЕРВИС ОБНОВЛЕНИЕ: Обновление графика работы клиники",
|
||||||
|
href: "#",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "12.06.2025",
|
||||||
|
title: "СЕРВИС ОБНОВЛЕНИЕ: Временное изменение работы клиники 22.06.25 г.",
|
||||||
|
href: "#",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "11.06.2025",
|
||||||
|
title: "График работы клиники в ближайшие праздники",
|
||||||
|
href: "#",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const LLM_NEWS_TEXT = `
|
||||||
|
БЛОК: Новости
|
||||||
|
Источник: perm.oclinica.ru/lor — блок новостей внизу страницы
|
||||||
|
Версия: v1.1
|
||||||
|
|
||||||
|
СТРУКТУРА БЛОКА:
|
||||||
|
1. ЗАГОЛОВОК H2: «Новости»
|
||||||
|
Выравнивание: по центру или слева
|
||||||
|
|
||||||
|
2. СЕТКА КАРТОЧЕК НОВОСТЕЙ (4 штуки в ряд):
|
||||||
|
Каждая карточка:
|
||||||
|
— Дата: малый текст сверху (формат DD.MM.YYYY), цвет #6b7280
|
||||||
|
— Заголовок новости: синяя ссылка (#0089c3), 14px, font-weight 500, hover underline
|
||||||
|
— Без превью-изображения
|
||||||
|
— Без описания (только дата + заголовок-ссылка)
|
||||||
|
Hover на карточке: bg #eef4d1, box-shadow 0 0 16px 0 #9e9e9a (bb-news-card)
|
||||||
|
|
||||||
|
3. КНОПКА «Все новости»:
|
||||||
|
Стиль: outline (#BF9975) или teal (#60959c), выровнена по центру под сеткой
|
||||||
|
На сайте: ссылка «Все новости»
|
||||||
|
|
||||||
|
ЦВЕТА:
|
||||||
|
Дата: #6b7280
|
||||||
|
Заголовок-ссылка: #0089c3 (--brand-053m)
|
||||||
|
Hover фон карточки: #eef4d1
|
||||||
|
Hover тень: 0 0 16px 0 #9e9e9a
|
||||||
|
|
||||||
|
ПРАВИЛА:
|
||||||
|
✓ 4 карточки в ряд
|
||||||
|
✓ Дата сверху карточки
|
||||||
|
✓ Заголовок = ссылка #0089c3
|
||||||
|
✓ Hover: bg #eef4d1 (класс bb-news-card)
|
||||||
|
✓ Кнопка «Все новости» под сеткой
|
||||||
|
✕ Не добавлять изображения в карточки новостей главной страницы
|
||||||
|
✕ Не добавлять описание/анонс в карточку
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
version: "v1.1",
|
||||||
|
date: "24.03.2026",
|
||||||
|
changes: [
|
||||||
|
"H2: размер на 36px, цвет на #000000, line-height 38px",
|
||||||
|
"Фон секции: с белого на #f2fee6 (светло-зелёный, как на реальном сайте)",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function NewsPageClient() {
|
||||||
|
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)" }}>
|
||||||
|
Блок «Новости»
|
||||||
|
</h1>
|
||||||
|
<BlockMetaBar
|
||||||
|
path={BLOCK_PATH}
|
||||||
|
defaultVersion="v1.1"
|
||||||
|
defaultIsInPreview={false}
|
||||||
|
defaultChangelog={CHANGELOG}
|
||||||
|
onSnapshotSelect={setSnapshot}
|
||||||
|
/>
|
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
Блок новостей с perm.oclinica.ru/lor — 4 карточки в ряд (дата + заголовок-ссылка),
|
||||||
|
кнопка «Все новости».
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Живой пример */}
|
||||||
|
<section className="space-y-4">
|
||||||
|
<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}>
|
||||||
|
<NewsBlock />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Анатомия карточки */}
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||||
|
Анатомия карточки новости
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
className="rounded-xl p-6 max-w-xs"
|
||||||
|
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}
|
||||||
|
>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 px-2 py-1 rounded text-xs font-mono"
|
||||||
|
style={{ background: "#f0f9ff", color: "var(--bb-text-muted)" }}
|
||||||
|
>
|
||||||
|
① Дата: DD.MM.YYYY · цвет #6b7280
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 px-2 py-1 rounded text-xs font-mono"
|
||||||
|
style={{ background: "#f0f9ff", color: "var(--bb-text-muted)" }}
|
||||||
|
>
|
||||||
|
② Заголовок-ссылка · #0089c3 · font-weight 500
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 px-2 py-1 rounded text-xs font-mono"
|
||||||
|
style={{ background: "#fefce8", color: "var(--bb-text-muted)" }}
|
||||||
|
>
|
||||||
|
Hover: bg #eef4d1 · shadow 0 0 16px #9e9e9a
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
Нет изображений, нет описания — только дата и заголовок.
|
||||||
|
CSS-класс <code className="font-mono">.bb-news-card</code> обрабатывает hover.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* LLM блок */}
|
||||||
|
<LlmBlock path="/blocks/news" version="v1.1" specText={LLM_NEWS_TEXT}>
|
||||||
|
<LlmSection title="Структура карточки новости" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Поле", "Стиль", "Значение"]}
|
||||||
|
rows={[
|
||||||
|
["Дата", "text-xs, #6b7280", "Формат DD.MM.YYYY"],
|
||||||
|
["Заголовок", "text-sm, font-weight 500, #0089c3", "Ссылка на новость"],
|
||||||
|
["Hover фон", ".bb-news-card:hover", "#eef4d1"],
|
||||||
|
["Hover тень", ".bb-news-card:hover", "0 0 16px 0 #9e9e9a"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Mock-данные (4 реальные новости с сайта)" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Дата", "Заголовок"]}
|
||||||
|
rows={MOCK_NEWS.map((n) => [n.date, n.title])}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Правила" />
|
||||||
|
<LlmRules
|
||||||
|
rules={[
|
||||||
|
{ ok: true, text: "4 карточки в ряд (grid-cols-4)" },
|
||||||
|
{ ok: true, text: "Дата сверху, заголовок-ссылка ниже" },
|
||||||
|
{ ok: true, text: "Hover: класс bb-news-card (bg #eef4d1)" },
|
||||||
|
{ ok: true, text: "Кнопка «Все новости» под сеткой (bb-btn-outline)" },
|
||||||
|
{ ok: false, text: "Не добавлять изображения в карточки" },
|
||||||
|
{ ok: false, text: "Не добавлять текст-анонс в карточку" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</LlmBlock>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,178 +1,10 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
import NewsPageClient from "./NewsPageClient";
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
|
||||||
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
|
||||||
import { NewsBlock } from "@/components/blocks/NewsBlock";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Блок «Новости». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
title: "Блок «Новости». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
};
|
};
|
||||||
|
|
||||||
const MOCK_NEWS = [
|
|
||||||
{
|
|
||||||
date: "20.12.2025",
|
|
||||||
title: "Наша работа клиники и новогодние праздники",
|
|
||||||
href: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "11.08.2025",
|
|
||||||
title: "СЕРВИС ОБНОВЛЕНИЕ: Обновление графика работы клиники",
|
|
||||||
href: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "12.06.2025",
|
|
||||||
title: "СЕРВИС ОБНОВЛЕНИЕ: Временное изменение работы клиники 22.06.25 г.",
|
|
||||||
href: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "11.06.2025",
|
|
||||||
title: "График работы клиники в ближайшие праздники",
|
|
||||||
href: "#",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const LLM_NEWS_TEXT = `
|
|
||||||
БЛОК: Новости
|
|
||||||
Источник: perm.oclinica.ru/lor — блок новостей внизу страницы
|
|
||||||
Версия: v1.1
|
|
||||||
|
|
||||||
СТРУКТУРА БЛОКА:
|
|
||||||
1. ЗАГОЛОВОК H2: «Новости»
|
|
||||||
Выравнивание: по центру или слева
|
|
||||||
|
|
||||||
2. СЕТКА КАРТОЧЕК НОВОСТЕЙ (4 штуки в ряд):
|
|
||||||
Каждая карточка:
|
|
||||||
— Дата: малый текст сверху (формат DD.MM.YYYY), цвет #6b7280
|
|
||||||
— Заголовок новости: синяя ссылка (#0089c3), 14px, font-weight 500, hover underline
|
|
||||||
— Без превью-изображения
|
|
||||||
— Без описания (только дата + заголовок-ссылка)
|
|
||||||
Hover на карточке: bg #eef4d1, box-shadow 0 0 16px 0 #9e9e9a (bb-news-card)
|
|
||||||
|
|
||||||
3. КНОПКА «Все новости»:
|
|
||||||
Стиль: outline (#BF9975) или teal (#60959c), выровнена по центру под сеткой
|
|
||||||
На сайте: ссылка «Все новости»
|
|
||||||
|
|
||||||
ЦВЕТА:
|
|
||||||
Дата: #6b7280
|
|
||||||
Заголовок-ссылка: #0089c3 (--brand-053m)
|
|
||||||
Hover фон карточки: #eef4d1
|
|
||||||
Hover тень: 0 0 16px 0 #9e9e9a
|
|
||||||
|
|
||||||
ПРАВИЛА:
|
|
||||||
✓ 4 карточки в ряд
|
|
||||||
✓ Дата сверху карточки
|
|
||||||
✓ Заголовок = ссылка #0089c3
|
|
||||||
✓ Hover: bg #eef4d1 (класс bb-news-card)
|
|
||||||
✓ Кнопка «Все новости» под сеткой
|
|
||||||
✕ Не добавлять изображения в карточки новостей главной страницы
|
|
||||||
✕ Не добавлять описание/анонс в карточку
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const CHANGELOG: ChangelogEntry[] = [
|
|
||||||
{
|
|
||||||
version: "v1.1",
|
|
||||||
date: "24.03.2026",
|
|
||||||
changes: [
|
|
||||||
"H2: размер на 36px, цвет на #000000, line-height 38px",
|
|
||||||
"Фон секции: с белого на #f2fee6 (светло-зелёный, как на реальном сайте)",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function NewsBlockPage() {
|
export default function NewsBlockPage() {
|
||||||
return (
|
return <NewsPageClient />;
|
||||||
<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)" }}>
|
|
||||||
Блок «Новости»
|
|
||||||
</h1>
|
|
||||||
<BlockMetaBar path="/blocks/news" defaultVersion="v1.1" defaultIsInPreview={false} defaultChangelog={CHANGELOG} />
|
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
Блок новостей с perm.oclinica.ru/lor — 4 карточки в ряд (дата + заголовок-ссылка),
|
|
||||||
кнопка «Все новости».
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Живой пример */}
|
|
||||||
<section className="space-y-4">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Живой пример
|
|
||||||
</h2>
|
|
||||||
<NewsBlock />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Анатомия карточки */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Анатомия карточки новости
|
|
||||||
</h2>
|
|
||||||
<div
|
|
||||||
className="rounded-xl p-6 max-w-xs"
|
|
||||||
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}
|
|
||||||
>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-2 px-2 py-1 rounded text-xs font-mono"
|
|
||||||
style={{ background: "#f0f9ff", color: "var(--bb-text-muted)" }}
|
|
||||||
>
|
|
||||||
① Дата: DD.MM.YYYY · цвет #6b7280
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-2 px-2 py-1 rounded text-xs font-mono"
|
|
||||||
style={{ background: "#f0f9ff", color: "var(--bb-text-muted)" }}
|
|
||||||
>
|
|
||||||
② Заголовок-ссылка · #0089c3 · font-weight 500
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-2 px-2 py-1 rounded text-xs font-mono"
|
|
||||||
style={{ background: "#fefce8", color: "var(--bb-text-muted)" }}
|
|
||||||
>
|
|
||||||
Hover: bg #eef4d1 · shadow 0 0 16px #9e9e9a
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
Нет изображений, нет описания — только дата и заголовок.
|
|
||||||
CSS-класс <code className="font-mono">.bb-news-card</code> обрабатывает hover.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* LLM блок */}
|
|
||||||
<LlmBlock path="/blocks/news" version="v1.1" specText={LLM_NEWS_TEXT}>
|
|
||||||
<LlmSection title="Структура карточки новости" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Поле", "Стиль", "Значение"]}
|
|
||||||
rows={[
|
|
||||||
["Дата", "text-xs, #6b7280", "Формат DD.MM.YYYY"],
|
|
||||||
["Заголовок", "text-sm, font-weight 500, #0089c3", "Ссылка на новость"],
|
|
||||||
["Hover фон", ".bb-news-card:hover", "#eef4d1"],
|
|
||||||
["Hover тень", ".bb-news-card:hover", "0 0 16px 0 #9e9e9a"],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Mock-данные (4 реальные новости с сайта)" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Дата", "Заголовок"]}
|
|
||||||
rows={MOCK_NEWS.map((n) => [n.date, n.title])}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Правила" />
|
|
||||||
<LlmRules
|
|
||||||
rules={[
|
|
||||||
{ ok: true, text: "4 карточки в ряд (grid-cols-4)" },
|
|
||||||
{ ok: true, text: "Дата сверху, заголовок-ссылка ниже" },
|
|
||||||
{ ok: true, text: "Hover: класс bb-news-card (bg #eef4d1)" },
|
|
||||||
{ ok: true, text: "Кнопка «Все новости» под сеткой (bb-btn-outline)" },
|
|
||||||
{ ok: false, text: "Не добавлять изображения в карточки" },
|
|
||||||
{ ok: false, text: "Не добавлять текст-анонс в карточку" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</LlmBlock>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||||
|
import { BlockMetaBar, type SnapshotData } from "@/components/ui/BlockMetaBar";
|
||||||
|
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
||||||
|
import { ReviewsBlock } from "@/components/blocks/ReviewsBlock";
|
||||||
|
|
||||||
|
const BLOCK_PATH = "/blocks/reviews";
|
||||||
|
|
||||||
|
const MOCK_REVIEWS = [
|
||||||
|
{
|
||||||
|
text: "Спасибо за приём, мне всё понравилось, спасибо за приём, мне всё понравилось. Врач очень внимательный и профессиональный.",
|
||||||
|
author: "Пациент клиники",
|
||||||
|
doctor: "Тимофеева Наталья Александровна",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Очень довольна лечением! Прошла курс процедур, нос дышит отлично. Рекомендую клинику всем.",
|
||||||
|
author: "Наталья К.",
|
||||||
|
doctor: "Макарова Людмила Тимофеевна",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const LLM_REVIEWS_TEXT = `
|
||||||
|
БЛОК: Отзывы о нас
|
||||||
|
Источник: perm.oclinica.ru/lor — блок отзывов
|
||||||
|
Версия: v1.1
|
||||||
|
|
||||||
|
СТРУКТУРА БЛОКА:
|
||||||
|
1. ЗАГОЛОВОК H2: «Отзывы о нас»
|
||||||
|
Подзаголовок: «За 12 лет работы наши врачи оториноларингологи избавили от болезней ухо, горло, носа
|
||||||
|
более 50000 пациентов. Но дня сейчас мы высоко ценим каждый положительный отзыв и искренние слова благодарности»
|
||||||
|
|
||||||
|
2. КАРУСЕЛЬ ОТЗЫВОВ:
|
||||||
|
— Большая открывающая кавычка (« «) в цвете #b8e6ed, font-size 80–100px
|
||||||
|
— Текст отзыва: курсив, 15–16px, #374151, background #eef4d1
|
||||||
|
— Ссылка «Читать отзыв полностью» в цвете #0089c3
|
||||||
|
— Стрелки навигации < > по бокам (round buttons)
|
||||||
|
— Карусель показывает 1 отзыв за раз
|
||||||
|
|
||||||
|
ЦВЕТА:
|
||||||
|
Фон карточки отзыва: #eef4d1 (светло-жёлто-зелёный)
|
||||||
|
Кавычка: #b8e6ed (светло-бирюзовый)
|
||||||
|
Текст отзыва: #374151
|
||||||
|
Ссылка «Читать полностью»: #0089c3
|
||||||
|
|
||||||
|
ПРАВИЛА:
|
||||||
|
✓ Фон карточки отзыва: #eef4d1 (тот же что у ReviewCard)
|
||||||
|
✓ Большая декоративная кавычка
|
||||||
|
✓ Ссылка «Читать отзыв полностью» обязательна
|
||||||
|
✓ Навигация карусели (стрелки)
|
||||||
|
✕ Не показывать более 1 отзыва за раз в карусели
|
||||||
|
✕ Не убирать навигационные стрелки
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
version: "v1.1",
|
||||||
|
date: "24.03.2026",
|
||||||
|
changes: [
|
||||||
|
"H2: размер с ~20px на 36px, цвет с #111827 на #000000, line-height 38px",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ReviewsPageClient() {
|
||||||
|
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)" }}>
|
||||||
|
Блок «Отзывы о нас»
|
||||||
|
</h1>
|
||||||
|
<BlockMetaBar
|
||||||
|
path={BLOCK_PATH}
|
||||||
|
defaultVersion="v1.1"
|
||||||
|
defaultIsInPreview={false}
|
||||||
|
defaultChangelog={CHANGELOG}
|
||||||
|
onSnapshotSelect={setSnapshot}
|
||||||
|
/>
|
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
Карусель отзывов с perm.oclinica.ru/lor — большая кавычка, текст, «Читать полностью», стрелки.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Живой пример */}
|
||||||
|
<section className="space-y-4">
|
||||||
|
<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}>
|
||||||
|
<ReviewsBlock />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Несколько примеров */}
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||||
|
Примеры отзывов (mock)
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{MOCK_REVIEWS.map((r, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="rounded-xl p-5 space-y-3"
|
||||||
|
style={{ background: "#eef4d1", border: "1px solid #d4e6a0" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="text-5xl leading-none font-serif"
|
||||||
|
style={{ color: "#b8e6ed" }}
|
||||||
|
>
|
||||||
|
«
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
className="text-sm italic leading-relaxed"
|
||||||
|
style={{ color: "#374151" }}
|
||||||
|
>
|
||||||
|
{r.text}
|
||||||
|
</p>
|
||||||
|
<div className="pt-1">
|
||||||
|
<a href="#" className="text-sm" style={{ color: "#0089c3" }}>
|
||||||
|
Читать отзыв полностью
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="border-t pt-2" style={{ borderColor: "#c8d8a0" }}>
|
||||||
|
<p className="text-xs font-medium" style={{ color: "#374151" }}>
|
||||||
|
{r.author}
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px]" style={{ color: "#6b7280" }}>
|
||||||
|
о враче: {r.doctor}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Анатомия */}
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||||
|
Анатомия карточки отзыва
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
|
{[
|
||||||
|
{ label: "Фон карточки", value: "#eef4d1", note: "светло-жёлто-зелёный" },
|
||||||
|
{ label: "Кавычка декоративная", value: "#b8e6ed", note: "80–100px, font-serif" },
|
||||||
|
{ label: "Текст отзыва", value: "#374151", note: "14px, italic" },
|
||||||
|
{ label: "Ссылка", value: "#0089c3", note: "--brand-053m" },
|
||||||
|
].map((t) => (
|
||||||
|
<div
|
||||||
|
key={t.label}
|
||||||
|
className="p-3 rounded-xl space-y-1"
|
||||||
|
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-8 h-8 rounded border"
|
||||||
|
style={{ background: t.value, borderColor: "var(--bb-border)" }}
|
||||||
|
/>
|
||||||
|
<p className="text-xs font-medium" style={{ color: "var(--bb-text)" }}>
|
||||||
|
{t.label}
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] font-mono" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
{t.value}
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px]" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
{t.note}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* LLM блок */}
|
||||||
|
<LlmBlock path="/blocks/reviews" version="v1.1" specText={LLM_REVIEWS_TEXT}>
|
||||||
|
<LlmSection title="Структура блока" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Элемент", "Содержимое", "Стиль"]}
|
||||||
|
rows={[
|
||||||
|
["H2", "«Отзывы о нас»", "36px, font-bold, #000000, line-height 38px"],
|
||||||
|
["Подзаголовок", "Описание достижений клиники за 12 лет", "14px, #374151"],
|
||||||
|
["Кавычка", "Декоративная «", "80–100px, #b8e6ed, font-serif"],
|
||||||
|
["Текст отзыва", "Полный текст отзыва пациента", "14px, italic, #374151"],
|
||||||
|
["Ссылка", "«Читать отзыв полностью»", "#0089c3"],
|
||||||
|
["Стрелки карусели", "‹ ›", "Round buttons, фон var(--bb-sidebar-bg)"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Цвета" />
|
||||||
|
<LlmTable
|
||||||
|
headers={["Элемент", "Цвет", "Токен"]}
|
||||||
|
rows={[
|
||||||
|
["Фон карточки отзыва", "#eef4d1", "—"],
|
||||||
|
["Декоративная кавычка", "#b8e6ed", "—"],
|
||||||
|
["Текст отзыва", "#374151", "—"],
|
||||||
|
["Ссылка", "#0089c3", "--brand-053m"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<LlmSection title="Правила" />
|
||||||
|
<LlmRules
|
||||||
|
rules={[
|
||||||
|
{ ok: true, text: "Фон карточки отзыва: #eef4d1" },
|
||||||
|
{ ok: true, text: "Декоративная кавычка цвет #b8e6ed" },
|
||||||
|
{ ok: true, text: "Ссылка «Читать отзыв полностью» обязательна" },
|
||||||
|
{ ok: true, text: "Навигация карусели: стрелки ‹ ›" },
|
||||||
|
{ ok: false, text: "Не показывать несколько отзывов одновременно" },
|
||||||
|
{ ok: false, text: "Не убирать навигационные стрелки" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</LlmBlock>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,209 +1,10 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
import ReviewsPageClient from "./ReviewsPageClient";
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
|
||||||
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
|
||||||
import { ReviewsBlock } from "@/components/blocks/ReviewsBlock";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Блок «Отзывы». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
title: "Блок «Отзывы». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
};
|
};
|
||||||
|
|
||||||
const MOCK_REVIEWS = [
|
|
||||||
{
|
|
||||||
text: "Спасибо за приём, мне всё понравилось, спасибо за приём, мне всё понравилось. Врач очень внимательный и профессиональный.",
|
|
||||||
author: "Пациент клиники",
|
|
||||||
doctor: "Тимофеева Наталья Александровна",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Очень довольна лечением! Прошла курс процедур, нос дышит отлично. Рекомендую клинику всем.",
|
|
||||||
author: "Наталья К.",
|
|
||||||
doctor: "Макарова Людмила Тимофеевна",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const LLM_REVIEWS_TEXT = `
|
|
||||||
БЛОК: Отзывы о нас
|
|
||||||
Источник: perm.oclinica.ru/lor — блок отзывов
|
|
||||||
Версия: v1.1
|
|
||||||
|
|
||||||
СТРУКТУРА БЛОКА:
|
|
||||||
1. ЗАГОЛОВОК H2: «Отзывы о нас»
|
|
||||||
Подзаголовок: «За 12 лет работы наши врачи оториноларингологи избавили от болезней ухо, горло, носа
|
|
||||||
более 50000 пациентов. Но дня сейчас мы высоко ценим каждый положительный отзыв и искренние слова благодарности»
|
|
||||||
|
|
||||||
2. КАРУСЕЛЬ ОТЗЫВОВ:
|
|
||||||
— Большая открывающая кавычка (« «) в цвете #b8e6ed, font-size 80–100px
|
|
||||||
— Текст отзыва: курсив, 15–16px, #374151, background #eef4d1
|
|
||||||
— Ссылка «Читать отзыв полностью» в цвете #0089c3
|
|
||||||
— Стрелки навигации < > по бокам (round buttons)
|
|
||||||
— Карусель показывает 1 отзыв за раз
|
|
||||||
|
|
||||||
ЦВЕТА:
|
|
||||||
Фон карточки отзыва: #eef4d1 (светло-жёлто-зелёный)
|
|
||||||
Кавычка: #b8e6ed (светло-бирюзовый)
|
|
||||||
Текст отзыва: #374151
|
|
||||||
Ссылка «Читать полностью»: #0089c3
|
|
||||||
|
|
||||||
ПРАВИЛА:
|
|
||||||
✓ Фон карточки отзыва: #eef4d1 (тот же что у ReviewCard)
|
|
||||||
✓ Большая декоративная кавычка
|
|
||||||
✓ Ссылка «Читать отзыв полностью» обязательна
|
|
||||||
✓ Навигация карусели (стрелки)
|
|
||||||
✕ Не показывать более 1 отзыва за раз в карусели
|
|
||||||
✕ Не убирать навигационные стрелки
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const CHANGELOG: ChangelogEntry[] = [
|
|
||||||
{
|
|
||||||
version: "v1.1",
|
|
||||||
date: "24.03.2026",
|
|
||||||
changes: [
|
|
||||||
"H2: размер с ~20px на 36px, цвет с #111827 на #000000, line-height 38px",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function ReviewsBlockPage() {
|
export default function ReviewsBlockPage() {
|
||||||
return (
|
return <ReviewsPageClient />;
|
||||||
<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)" }}>
|
|
||||||
Блок «Отзывы о нас»
|
|
||||||
</h1>
|
|
||||||
<BlockMetaBar path="/blocks/reviews" defaultVersion="v1.1" defaultIsInPreview={false} defaultChangelog={CHANGELOG} />
|
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
Карусель отзывов с perm.oclinica.ru/lor — большая кавычка, текст, «Читать полностью», стрелки.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Живой пример */}
|
|
||||||
<section className="space-y-4">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Живой пример
|
|
||||||
</h2>
|
|
||||||
<ReviewsBlock />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Несколько примеров */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Примеры отзывов (mock)
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{MOCK_REVIEWS.map((r, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="rounded-xl p-5 space-y-3"
|
|
||||||
style={{ background: "#eef4d1", border: "1px solid #d4e6a0" }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="text-5xl leading-none font-serif"
|
|
||||||
style={{ color: "#b8e6ed" }}
|
|
||||||
>
|
|
||||||
«
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
className="text-sm italic leading-relaxed"
|
|
||||||
style={{ color: "#374151" }}
|
|
||||||
>
|
|
||||||
{r.text}
|
|
||||||
</p>
|
|
||||||
<div className="pt-1">
|
|
||||||
<a href="#" className="text-sm" style={{ color: "#0089c3" }}>
|
|
||||||
Читать отзыв полностью
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="border-t pt-2" style={{ borderColor: "#c8d8a0" }}>
|
|
||||||
<p className="text-xs font-medium" style={{ color: "#374151" }}>
|
|
||||||
{r.author}
|
|
||||||
</p>
|
|
||||||
<p className="text-[11px]" style={{ color: "#6b7280" }}>
|
|
||||||
о враче: {r.doctor}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Анатомия */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Анатомия карточки отзыва
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
||||||
{[
|
|
||||||
{ label: "Фон карточки", value: "#eef4d1", note: "светло-жёлто-зелёный" },
|
|
||||||
{ label: "Кавычка декоративная", value: "#b8e6ed", note: "80–100px, font-serif" },
|
|
||||||
{ label: "Текст отзыва", value: "#374151", note: "14px, italic" },
|
|
||||||
{ label: "Ссылка", value: "#0089c3", note: "--brand-053m" },
|
|
||||||
].map((t) => (
|
|
||||||
<div
|
|
||||||
key={t.label}
|
|
||||||
className="p-3 rounded-xl space-y-1"
|
|
||||||
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="w-8 h-8 rounded border"
|
|
||||||
style={{ background: t.value, borderColor: "var(--bb-border)" }}
|
|
||||||
/>
|
|
||||||
<p className="text-xs font-medium" style={{ color: "var(--bb-text)" }}>
|
|
||||||
{t.label}
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px] font-mono" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
{t.value}
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px]" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
{t.note}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* LLM блок */}
|
|
||||||
<LlmBlock path="/blocks/reviews" version="v1.1" specText={LLM_REVIEWS_TEXT}>
|
|
||||||
<LlmSection title="Структура блока" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Элемент", "Содержимое", "Стиль"]}
|
|
||||||
rows={[
|
|
||||||
["H2", "«Отзывы о нас»", "36px, font-bold, #000000, line-height 38px"],
|
|
||||||
["Подзаголовок", "Описание достижений клиники за 12 лет", "14px, #374151"],
|
|
||||||
["Кавычка", "Декоративная «", "80–100px, #b8e6ed, font-serif"],
|
|
||||||
["Текст отзыва", "Полный текст отзыва пациента", "14px, italic, #374151"],
|
|
||||||
["Ссылка", "«Читать отзыв полностью»", "#0089c3"],
|
|
||||||
["Стрелки карусели", "‹ ›", "Round buttons, фон var(--bb-sidebar-bg)"],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Цвета" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Элемент", "Цвет", "Токен"]}
|
|
||||||
rows={[
|
|
||||||
["Фон карточки отзыва", "#eef4d1", "—"],
|
|
||||||
["Декоративная кавычка", "#b8e6ed", "—"],
|
|
||||||
["Текст отзыва", "#374151", "—"],
|
|
||||||
["Ссылка", "#0089c3", "--brand-053m"],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Правила" />
|
|
||||||
<LlmRules
|
|
||||||
rules={[
|
|
||||||
{ ok: true, text: "Фон карточки отзыва: #eef4d1" },
|
|
||||||
{ ok: true, text: "Декоративная кавычка цвет #b8e6ed" },
|
|
||||||
{ ok: true, text: "Ссылка «Читать отзыв полностью» обязательна" },
|
|
||||||
{ ok: true, text: "Навигация карусели: стрелки ‹ ›" },
|
|
||||||
{ ok: false, text: "Не показывать несколько отзывов одновременно" },
|
|
||||||
{ ok: false, text: "Не убирать навигационные стрелки" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</LlmBlock>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,14 +25,47 @@ interface BlockMeta {
|
|||||||
changelog: ChangelogEntry[];
|
changelog: ChangelogEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SnapshotListItem {
|
||||||
|
id: string;
|
||||||
|
version: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SnapshotData {
|
||||||
|
html: string;
|
||||||
|
css: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface BlockMetaBarProps {
|
interface BlockMetaBarProps {
|
||||||
path: string;
|
path: string;
|
||||||
defaultVersion: string;
|
defaultVersion: string;
|
||||||
defaultIsInPreview: boolean;
|
defaultIsInPreview: boolean;
|
||||||
defaultChangelog?: ChangelogEntry[];
|
defaultChangelog?: ChangelogEntry[];
|
||||||
|
onSnapshotSelect?: (snapshot: SnapshotData | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview, defaultChangelog = [] }: BlockMetaBarProps) {
|
function captureBlockHtml(path: string): { html: string; css: string } {
|
||||||
|
const container = document.querySelector(`[data-block-capture="${path}"]`);
|
||||||
|
if (!container) throw new Error("Block container not found");
|
||||||
|
|
||||||
|
const html = container.innerHTML;
|
||||||
|
|
||||||
|
const cssRules: string[] = [];
|
||||||
|
for (const sheet of document.styleSheets) {
|
||||||
|
try {
|
||||||
|
for (const rule of sheet.cssRules) {
|
||||||
|
const text = rule.cssText;
|
||||||
|
if (text.includes("bb-") || text.includes("--brand") || text.includes("--bb-")) {
|
||||||
|
cssRules.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* cross-origin sheets */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { html, css: cssRules.join("\n") };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview, defaultChangelog = [], onSnapshotSelect }: BlockMetaBarProps) {
|
||||||
const [meta, setMeta] = useState<BlockMeta | null>(null);
|
const [meta, setMeta] = useState<BlockMeta | null>(null);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [versionInput, setVersionInput] = useState(defaultVersion);
|
const [versionInput, setVersionInput] = useState(defaultVersion);
|
||||||
@@ -41,6 +74,9 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview, default
|
|||||||
const [togglingPreview, setTogglingPreview] = useState(false);
|
const [togglingPreview, setTogglingPreview] = useState(false);
|
||||||
const [apiDown, setApiDown] = useState(false);
|
const [apiDown, setApiDown] = useState(false);
|
||||||
const [localPreview, setLocalPreview] = useState(defaultIsInPreview);
|
const [localPreview, setLocalPreview] = useState(defaultIsInPreview);
|
||||||
|
const [snapshots, setSnapshots] = useState<SnapshotListItem[]>([]);
|
||||||
|
const [selectedSnapshotId, setSelectedSnapshotId] = useState<string>("current");
|
||||||
|
const [loadingSnapshot, setLoadingSnapshot] = useState(false);
|
||||||
|
|
||||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
|
|
||||||
@@ -59,6 +95,20 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview, default
|
|||||||
.catch(() => setApiDown(true));
|
.catch(() => setApiDown(true));
|
||||||
}, [apiUrl, path]);
|
}, [apiUrl, path]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!apiUrl || apiDown) return;
|
||||||
|
fetchSnapshots();
|
||||||
|
}, [apiUrl, apiDown, path]);
|
||||||
|
|
||||||
|
async function fetchSnapshots() {
|
||||||
|
if (!apiUrl) return;
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${apiUrl}/blocks/snapshots?path=${encodeURIComponent(path)}`);
|
||||||
|
const data: SnapshotListItem[] = await r.json();
|
||||||
|
setSnapshots(data);
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
async function patch(data: { version?: string; isInPreview?: boolean; changelog?: ChangelogEntry[] }) {
|
async function patch(data: { version?: string; isInPreview?: boolean; changelog?: ChangelogEntry[] }) {
|
||||||
if (!apiUrl) return null;
|
if (!apiUrl) return null;
|
||||||
const r = await fetch(`${apiUrl}/blocks/by-path?path=${encodeURIComponent(path)}`, {
|
const r = await fetch(`${apiUrl}/blocks/by-path?path=${encodeURIComponent(path)}`, {
|
||||||
@@ -81,14 +131,27 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview, default
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveFullVersion() {
|
async function saveFullVersion() {
|
||||||
if (apiDown) return;
|
if (apiDown || !apiUrl) return;
|
||||||
setSavingVersion('saving');
|
setSavingVersion('saving');
|
||||||
try {
|
try {
|
||||||
|
// 1. Update block metadata
|
||||||
const updated = await patch({ version: defaultVersion, changelog: defaultChangelog });
|
const updated = await patch({ version: defaultVersion, changelog: defaultChangelog });
|
||||||
if (updated) {
|
if (updated) {
|
||||||
setMeta(updated);
|
setMeta(updated);
|
||||||
setVersionInput(updated.version);
|
setVersionInput(updated.version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Capture and save HTML snapshot
|
||||||
|
try {
|
||||||
|
const { html, css } = captureBlockHtml(path);
|
||||||
|
await fetch(`${apiUrl}/blocks/snapshots?path=${encodeURIComponent(path)}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ version: defaultVersion, changelog: defaultChangelog, html, css }),
|
||||||
|
});
|
||||||
|
await fetchSnapshots();
|
||||||
|
} catch { /* snapshot capture may fail if container not found */ }
|
||||||
|
|
||||||
setSavingVersion('done');
|
setSavingVersion('done');
|
||||||
setTimeout(() => setSavingVersion(false), 1500);
|
setTimeout(() => setSavingVersion(false), 1500);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -113,9 +176,30 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview, default
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleVersionSelect(value: string) {
|
||||||
|
setSelectedSnapshotId(value);
|
||||||
|
if (value === "current") {
|
||||||
|
onSnapshotSelect?.(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!apiUrl) return;
|
||||||
|
setLoadingSnapshot(true);
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${apiUrl}/blocks/snapshots/${value}`);
|
||||||
|
const data = await r.json();
|
||||||
|
onSnapshotSelect?.({ html: data.html, css: data.css });
|
||||||
|
} catch {
|
||||||
|
onSnapshotSelect?.(null);
|
||||||
|
setSelectedSnapshotId("current");
|
||||||
|
} finally {
|
||||||
|
setLoadingSnapshot(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const version = meta?.version ?? defaultVersion;
|
const version = meta?.version ?? defaultVersion;
|
||||||
const isInPreview = apiDown ? localPreview : (meta?.isInPreview ?? localPreview);
|
const isInPreview = apiDown ? localPreview : (meta?.isInPreview ?? localPreview);
|
||||||
const changelog: ChangelogEntry[] = (meta?.changelog && meta.changelog.length > 0) ? meta.changelog : defaultChangelog;
|
const changelog: ChangelogEntry[] = (meta?.changelog && meta.changelog.length > 0) ? meta.changelog : defaultChangelog;
|
||||||
|
const isViewingSnapshot = selectedSnapshotId !== "current";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -123,11 +207,31 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview, default
|
|||||||
className="flex items-center flex-wrap gap-3 px-4 py-2 rounded-lg text-xs mb-6"
|
className="flex items-center flex-wrap gap-3 px-4 py-2 rounded-lg text-xs mb-6"
|
||||||
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}
|
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}
|
||||||
>
|
>
|
||||||
{/* Version badge */}
|
{/* Version selector */}
|
||||||
<span className="font-semibold" style={{ color: "var(--bb-text-muted)" }}>
|
<span className="font-semibold" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
Версия:
|
Версия:
|
||||||
</span>
|
</span>
|
||||||
{editing ? (
|
{snapshots.length > 0 ? (
|
||||||
|
<select
|
||||||
|
value={selectedSnapshotId}
|
||||||
|
onChange={(e) => handleVersionSelect(e.target.value)}
|
||||||
|
disabled={loadingSnapshot}
|
||||||
|
className="font-mono px-2 py-0.5 rounded text-xs"
|
||||||
|
style={{
|
||||||
|
background: "var(--bb-sidebar-active-bg, #dff0fa)",
|
||||||
|
color: "var(--brand-053m)",
|
||||||
|
border: "1px solid var(--bb-border)",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="current">{version} (текущая)</option>
|
||||||
|
{snapshots.map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>
|
||||||
|
{s.version} — {new Date(s.createdAt).toLocaleDateString("ru")}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
) : editing ? (
|
||||||
<span className="flex items-center gap-1.5">
|
<span className="flex items-center gap-1.5">
|
||||||
<input
|
<input
|
||||||
className="px-2 py-0.5 rounded border text-xs font-mono w-20"
|
className="px-2 py-0.5 rounded border text-xs font-mono w-20"
|
||||||
@@ -168,6 +272,10 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview, default
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{loadingSnapshot && (
|
||||||
|
<span className="text-xs" style={{ color: "var(--bb-text-muted)" }}>загрузка...</span>
|
||||||
|
)}
|
||||||
|
|
||||||
<span style={{ color: "var(--bb-border)" }}>·</span>
|
<span style={{ color: "var(--bb-border)" }}>·</span>
|
||||||
|
|
||||||
{/* Preview toggle */}
|
{/* Preview toggle */}
|
||||||
@@ -202,8 +310,8 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview, default
|
|||||||
{/* Save version button */}
|
{/* Save version button */}
|
||||||
<button
|
<button
|
||||||
onClick={saveFullVersion}
|
onClick={saveFullVersion}
|
||||||
disabled={apiDown || savingVersion === 'saving'}
|
disabled={apiDown || savingVersion === 'saving' || isViewingSnapshot}
|
||||||
title={apiDown ? 'API недоступен' : 'Сохранить текущую версию и changelog в БД'}
|
title={apiDown ? 'API недоступен' : isViewingSnapshot ? 'Переключитесь на текущую версию' : 'Сохранить текущую версию и changelog в БД'}
|
||||||
className="px-2.5 py-1 rounded font-medium transition-colors"
|
className="px-2.5 py-1 rounded font-medium transition-colors"
|
||||||
style={savingVersion === 'done' ? {
|
style={savingVersion === 'done' ? {
|
||||||
background: "#dcfce7",
|
background: "#dcfce7",
|
||||||
@@ -212,16 +320,16 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview, default
|
|||||||
cursor: "default",
|
cursor: "default",
|
||||||
} : {
|
} : {
|
||||||
background: "var(--bb-sidebar-bg)",
|
background: "var(--bb-sidebar-bg)",
|
||||||
color: apiDown ? "var(--bb-text-muted)" : "var(--brand-053m)",
|
color: (apiDown || isViewingSnapshot) ? "var(--bb-text-muted)" : "var(--brand-053m)",
|
||||||
border: "1px solid var(--bb-border)",
|
border: "1px solid var(--bb-border)",
|
||||||
cursor: apiDown ? "default" : "pointer",
|
cursor: (apiDown || isViewingSnapshot) ? "default" : "pointer",
|
||||||
opacity: apiDown ? 0.5 : 1,
|
opacity: (apiDown || isViewingSnapshot) ? 0.5 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{savingVersion === 'saving' ? '...' : savingVersion === 'done' ? 'Сохранено' : 'Сохранить версию'}
|
{savingVersion === 'saving' ? '...' : savingVersion === 'done' ? 'Сохранено' : 'Сохранить версию'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Subtle offline dot instead of "API офлайн" text */}
|
{/* Subtle offline dot */}
|
||||||
{apiDown && (
|
{apiDown && (
|
||||||
<span
|
<span
|
||||||
className="inline-block w-1.5 h-1.5 rounded-full"
|
className="inline-block w-1.5 h-1.5 rounded-full"
|
||||||
@@ -231,6 +339,23 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview, default
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Archive notice */}
|
||||||
|
{isViewingSnapshot && (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 px-4 py-2 rounded-lg text-xs mb-4"
|
||||||
|
style={{ background: "#fef3c7", border: "1px solid #fcd34d", color: "#92400e" }}
|
||||||
|
>
|
||||||
|
Архивная версия (только просмотр).
|
||||||
|
<button
|
||||||
|
onClick={() => handleVersionSelect("current")}
|
||||||
|
className="underline font-medium"
|
||||||
|
style={{ color: "#92400e" }}
|
||||||
|
>
|
||||||
|
Вернуться к текущей
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Changelog rendered from API or fallback */}
|
{/* Changelog rendered from API or fallback */}
|
||||||
{changelog.length > 0 && <BlockChangelog changelog={changelog} />}
|
{changelog.length > 0 && <BlockChangelog changelog={changelog} />}
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user