feat(sprint-5.5): store block metadata (version, changelog) in PostgreSQL
- Prisma schema: added `changelog Json @default("[]")` to Block model
- Migration: 20260324141120_add_changelog_field
- Seed: 8 blocks with actual versions (v1.0–v1.2) and changelog entries
- API: PATCH /blocks/by-path accepts changelog field
- CORS: accept any localhost port (regex pattern)
- BlockChangelog component: renders version history from API or fallback
- BlockMetaBar: loads changelog from API, passes to BlockChangelog
- Removed "API офлайн" text, replaced with subtle gray dot
- Added defaultChangelog prop for offline fallback
- Block pages: removed hardcoded changelog JSX, use defaultChangelog prop
- Updated SPRINTS.md with completed tasks
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Block" ADD COLUMN "changelog" JSONB NOT NULL DEFAULT '[]';
|
||||||
@@ -46,6 +46,7 @@ model Block {
|
|||||||
name String
|
name String
|
||||||
version String
|
version String
|
||||||
isInPreview Boolean @default(false)
|
isInPreview Boolean @default(false)
|
||||||
|
changelog Json @default("[]")
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-9
@@ -6,21 +6,120 @@ const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
|||||||
const prisma = new PrismaClient({ adapter });
|
const prisma = new PrismaClient({ adapter });
|
||||||
|
|
||||||
const BLOCKS = [
|
const BLOCKS = [
|
||||||
{ path: '/components/navigation', name: 'Шапка / Навигация', version: 'v1.0', isInPreview: true },
|
{
|
||||||
{ path: '/blocks/hero', name: 'Hero-баннер', version: 'v1.1', isInPreview: true },
|
path: '/components/navigation',
|
||||||
{ path: '/blocks/ceo', name: 'Вводный текст (CEO-блок)', version: 'v0.1', isInPreview: false },
|
name: 'Шапка / Навигация',
|
||||||
{ path: '/blocks/doctors', name: 'Наши врачи', version: 'v1.1', isInPreview: true },
|
version: 'v1.0',
|
||||||
{ path: '/blocks/reviews', name: 'Отзывы', version: 'v0.1', isInPreview: false },
|
isInPreview: true,
|
||||||
{ path: '/blocks/contact-forms', name: 'Формы записи', version: 'v0.1', isInPreview: false },
|
changelog: [
|
||||||
{ path: '/blocks/news', name: 'Новости', version: 'v0.1', isInPreview: false },
|
{ version: 'v1.0', date: '23.03.2026', changes: ['Топ-бар, логотип, главное меню из 8 пунктов'] },
|
||||||
{ path: '/blocks/contact', name: 'Подвал / Контакт', version: 'v0.1', isInPreview: false },
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/blocks/hero',
|
||||||
|
name: 'Hero-баннер',
|
||||||
|
version: 'v1.2',
|
||||||
|
isInPreview: true,
|
||||||
|
changelog: [
|
||||||
|
{ version: 'v1.2', date: '24.03.2026', changes: [
|
||||||
|
'H1: цвет #cb9768, размер 36px (было ~20px #53514e)',
|
||||||
|
'Заголовок баннера: 22px #333 (было 16px #111827)',
|
||||||
|
'CTA-кнопка: pill-стиль (было outline)',
|
||||||
|
'Дефис в H1: «–» → «-»',
|
||||||
|
]},
|
||||||
|
{ version: 'v1.1', date: '23.03.2026', changes: [
|
||||||
|
'Единый фон #f9f4e7 (ранее разбит на две зоны)',
|
||||||
|
'Реальное фото врача с пациентом',
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/blocks/ceo',
|
||||||
|
name: 'Вводный текст (CEO-блок)',
|
||||||
|
version: 'v1.0',
|
||||||
|
isInPreview: false,
|
||||||
|
changelog: [
|
||||||
|
{ version: 'v1.0', date: '23.03.2026', changes: ['Текст специализации клиники, вопросы-стимулы'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/blocks/doctors',
|
||||||
|
name: 'Наши врачи',
|
||||||
|
version: 'v1.2',
|
||||||
|
isInPreview: true,
|
||||||
|
changelog: [
|
||||||
|
{ version: 'v1.2', date: '24.03.2026', changes: [
|
||||||
|
'H2: 36px #000000 (было ~30px #111827)',
|
||||||
|
'line-height: 38px',
|
||||||
|
]},
|
||||||
|
{ version: 'v1.1', date: '23.03.2026', changes: [
|
||||||
|
'6 реальных фото врачей с сайта',
|
||||||
|
'Статистика без фона, только border-bottom #60959c',
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/blocks/reviews',
|
||||||
|
name: 'Отзывы',
|
||||||
|
version: 'v1.1',
|
||||||
|
isInPreview: true,
|
||||||
|
changelog: [
|
||||||
|
{ version: 'v1.1', date: '24.03.2026', changes: [
|
||||||
|
'H2: 36px #000000 (было ~20px #111827)',
|
||||||
|
'line-height: 38px',
|
||||||
|
]},
|
||||||
|
{ version: 'v1.0', date: '23.03.2026', changes: [
|
||||||
|
'Карусель отзывов: кавычка, текст, стрелки',
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/blocks/contact-forms',
|
||||||
|
name: 'Формы записи',
|
||||||
|
version: 'v1.1',
|
||||||
|
isInPreview: true,
|
||||||
|
changelog: [
|
||||||
|
{ version: 'v1.1', date: '24.03.2026', changes: [
|
||||||
|
'H2: 36px #000000',
|
||||||
|
'Фон формы 1: #b8e6ed → #d4f6f8',
|
||||||
|
'Фон формы 2: #ffffff → #d4f6f8 (обе формы на одном фоне)',
|
||||||
|
]},
|
||||||
|
{ version: 'v1.0', date: '23.03.2026', changes: [
|
||||||
|
'Две формы записи: «Будьте здоровы!» и «Узнайте стоимость операции»',
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/blocks/news',
|
||||||
|
name: 'Новости',
|
||||||
|
version: 'v1.1',
|
||||||
|
isInPreview: true,
|
||||||
|
changelog: [
|
||||||
|
{ version: 'v1.1', date: '24.03.2026', changes: [
|
||||||
|
'H2: 36px #000000',
|
||||||
|
'Фон секции: #fff → #f2fee6 (светло-зелёный)',
|
||||||
|
]},
|
||||||
|
{ version: 'v1.0', date: '23.03.2026', changes: [
|
||||||
|
'4 карточки новостей в ряд, кнопка «Все новости»',
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/blocks/contact',
|
||||||
|
name: 'Подвал / Контакт',
|
||||||
|
version: 'v1.0',
|
||||||
|
isInPreview: false,
|
||||||
|
changelog: [
|
||||||
|
{ version: 'v1.0', date: '23.03.2026', changes: ['4 колонки ссылок, адрес, часы работы'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
for (const block of BLOCKS) {
|
for (const block of BLOCKS) {
|
||||||
await prisma.block.upsert({
|
await prisma.block.upsert({
|
||||||
where: { path: block.path },
|
where: { path: block.path },
|
||||||
update: {},
|
update: { version: block.version, isInPreview: block.isInPreview, changelog: block.changelog },
|
||||||
create: block,
|
create: block,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export class BlocksController {
|
|||||||
@Patch('by-path')
|
@Patch('by-path')
|
||||||
update(
|
update(
|
||||||
@Query('path') path: string,
|
@Query('path') path: string,
|
||||||
@Body() body: { version?: string; isInPreview?: boolean },
|
@Body() body: { version?: string; isInPreview?: boolean; changelog?: object[] },
|
||||||
) {
|
) {
|
||||||
return this.blocks.update(path, body);
|
return this.blocks.update(path, body);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export class BlocksService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
update(path: string, data: { version?: string; isInPreview?: boolean }) {
|
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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { AppModule } from './app.module';
|
|||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
app.enableCors({ origin: 'http://localhost:3001' });
|
app.enableCors({ origin: [/^http:\/\/localhost:\d+$/] });
|
||||||
await app.listen(process.env.PORT ?? 3000);
|
await app.listen(process.env.PORT ?? 3000);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
||||||
|
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
||||||
import { ContactFormsBlock } from "@/components/blocks/ContactFormsBlock";
|
import { ContactFormsBlock } from "@/components/blocks/ContactFormsBlock";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -50,6 +51,18 @@ const LLM_FORMS_TEXT = `
|
|||||||
✕ Не убирать чекбокс согласия
|
✕ Не убирать чекбокс согласия
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
|
const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
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 (
|
||||||
<div className="p-8 max-w-5xl mx-auto space-y-10">
|
<div className="p-8 max-w-5xl mx-auto space-y-10">
|
||||||
@@ -64,7 +77,7 @@ export default function ContactFormsPage() {
|
|||||||
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}>
|
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}>
|
||||||
Формы записи
|
Формы записи
|
||||||
</h1>
|
</h1>
|
||||||
<BlockMetaBar path="/blocks/contact-forms" defaultVersion="v1.1" defaultIsInPreview={false} />
|
<BlockMetaBar path="/blocks/contact-forms" defaultVersion="v1.1" defaultIsInPreview={false} defaultChangelog={CHANGELOG} />
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
Два блока форм с perm.oclinica.ru/lor — запись на приём и запрос стоимости операции.
|
Два блока форм с perm.oclinica.ru/lor — запись на приём и запрос стоимости операции.
|
||||||
</p>
|
</p>
|
||||||
@@ -112,21 +125,6 @@ export default function ContactFormsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Changelog */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
История версий
|
|
||||||
</h2>
|
|
||||||
<div className="p-3 rounded-lg" style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}>
|
|
||||||
<p className="font-semibold text-xs mb-1" style={{ color: "var(--bb-text)" }}>v1.1 — 24.03.2026</p>
|
|
||||||
<ul className="list-disc list-inside space-y-0.5 text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
<li>H2: размер на <strong>36px</strong>, цвет на <strong>#000000</strong>, line-height 38px</li>
|
|
||||||
<li>Фон формы 1: с #b8e6ed на <strong>#d4f6f8</strong></li>
|
|
||||||
<li>Фон формы 2: с #ffffff на <strong>#d4f6f8</strong> (обе формы на одном фоне)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* LLM блок */}
|
{/* LLM блок */}
|
||||||
<LlmBlock path="/blocks/contact-forms" version="v1.1" specText={LLM_FORMS_TEXT}>
|
<LlmBlock path="/blocks/contact-forms" version="v1.1" specText={LLM_FORMS_TEXT}>
|
||||||
<LlmSection title="Форма 1 — Запись на приём" />
|
<LlmSection title="Форма 1 — Запись на приём" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||||
import { DoctorsBlock, STATS, DOCTORS } from "@/components/blocks/DoctorsBlock";
|
import { DoctorsBlock, STATS, DOCTORS } from "@/components/blocks/DoctorsBlock";
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
||||||
|
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Блок «Наши врачи». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
title: "Блок «Наши врачи». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
@@ -45,6 +46,25 @@ const LLM_DOCTORS_TEXT = `
|
|||||||
✕ Не убирать статистику
|
✕ Не убирать статистику
|
||||||
`.trim();
|
`.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 (
|
||||||
<div className="p-8 max-w-5xl mx-auto space-y-10">
|
<div className="p-8 max-w-5xl mx-auto space-y-10">
|
||||||
@@ -59,7 +79,7 @@ export default function DoctorsBlockPage() {
|
|||||||
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}>
|
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}>
|
||||||
Блок «Наши врачи»
|
Блок «Наши врачи»
|
||||||
</h1>
|
</h1>
|
||||||
<BlockMetaBar path="/blocks/doctors" defaultVersion="v1.2" defaultIsInPreview={true} />
|
<BlockMetaBar path="/blocks/doctors" defaultVersion="v1.2" defaultIsInPreview={true} defaultChangelog={CHANGELOG} />
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
Блок на странице perm.oclinica.ru/lor — заголовок, 3 стат-блока, сетка из 6 карточек врачей.
|
Блок на странице perm.oclinica.ru/lor — заголовок, 3 стат-блока, сетка из 6 карточек врачей.
|
||||||
</p>
|
</p>
|
||||||
@@ -101,29 +121,6 @@ export default function DoctorsBlockPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Changelog */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
История версий
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-2 text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
<div className="p-3 rounded-lg" style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}>
|
|
||||||
<p className="font-semibold text-xs mb-1" style={{ color: "var(--bb-text)" }}>v1.2 — 24.03.2026</p>
|
|
||||||
<ul className="list-disc list-inside space-y-0.5 text-xs">
|
|
||||||
<li>H2: размер с ~30px на <strong>36px</strong>, цвет с #111827 на <strong>#000000</strong></li>
|
|
||||||
<li>H2 line-height: <strong>38px</strong></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 rounded-lg" style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}>
|
|
||||||
<p className="font-semibold text-xs mb-1" style={{ color: "var(--bb-text)" }}>v1.1 — 23.03.2026</p>
|
|
||||||
<ul className="list-disc list-inside space-y-0.5 text-xs">
|
|
||||||
<li>6 реальных фото врачей с сайта</li>
|
|
||||||
<li>Статистика без фона, только border-bottom #60959c</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* LLM блок */}
|
{/* LLM блок */}
|
||||||
<LlmBlock path="/blocks/doctors" version="v1.2" specText={LLM_DOCTORS_TEXT}>
|
<LlmBlock path="/blocks/doctors" version="v1.2" specText={LLM_DOCTORS_TEXT}>
|
||||||
<LlmSection title="Структура блока" />
|
<LlmSection title="Структура блока" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||||
import { HeroBlock, HERO_CHECKS } from "@/components/blocks/HeroBlock";
|
import { HeroBlock, HERO_CHECKS } from "@/components/blocks/HeroBlock";
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
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-баннер. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
@@ -50,6 +51,27 @@ const LLM_HERO_TEXT = `
|
|||||||
✕ Не убирать три пункта с галочками
|
✕ Не убирать три пункта с галочками
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
|
const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
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 (
|
||||||
<div className="p-8 max-w-5xl mx-auto space-y-10">
|
<div className="p-8 max-w-5xl mx-auto space-y-10">
|
||||||
@@ -64,7 +86,7 @@ export default function HeroPage() {
|
|||||||
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}>
|
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}>
|
||||||
Hero-баннер
|
Hero-баннер
|
||||||
</h1>
|
</h1>
|
||||||
<BlockMetaBar path="/blocks/hero" defaultVersion="v1.2" defaultIsInPreview={true} />
|
<BlockMetaBar path="/blocks/hero" defaultVersion="v1.2" defaultIsInPreview={true} defaultChangelog={CHANGELOG} />
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
Главный баннер страницы раздела ЛОР — perm.oclinica.ru/lor. Двухколоночный блок, единый светло-кремовый фон{" "}
|
Главный баннер страницы раздела ЛОР — perm.oclinica.ru/lor. Двухколоночный блок, единый светло-кремовый фон{" "}
|
||||||
<strong>#f9f4e7</strong>.
|
<strong>#f9f4e7</strong>.
|
||||||
@@ -138,31 +160,6 @@ export default function HeroPage() {
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Changelog */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
История версий
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-2 text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
<div className="p-3 rounded-lg" style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}>
|
|
||||||
<p className="font-semibold text-xs mb-1" style={{ color: "var(--bb-text)" }}>v1.2 — 24.03.2026</p>
|
|
||||||
<ul className="list-disc list-inside space-y-0.5 text-xs">
|
|
||||||
<li>H1: цвет исправлен с #53514e на <strong>#cb9768</strong>, размер с ~20px на <strong>36px</strong></li>
|
|
||||||
<li>Заголовок баннера: размер с ~16px на <strong>22px</strong>, цвет с #111827 на <strong>#333333</strong></li>
|
|
||||||
<li>CTA-кнопка: стиль изменён с outline на <strong>pill</strong> (фон #E9E4D4, radius 25px)</li>
|
|
||||||
<li>Дефис в H1: длинное тире «–» заменено на простой дефис «-» (как на сайте)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 rounded-lg" style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}>
|
|
||||||
<p className="font-semibold text-xs mb-1" style={{ color: "var(--bb-text)" }}>v1.1 — 23.03.2026</p>
|
|
||||||
<ul className="list-disc list-inside space-y-0.5 text-xs">
|
|
||||||
<li>Единый фон #f9f4e7 (ранее был разбит на две зоны)</li>
|
|
||||||
<li>Реальное фото врача с пациентом</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* LLM блок */}
|
{/* LLM блок */}
|
||||||
<LlmBlock path="/blocks/hero" version="v1.2" specText={LLM_HERO_TEXT}>
|
<LlmBlock path="/blocks/hero" version="v1.2" specText={LLM_HERO_TEXT}>
|
||||||
<LlmSection title="Структура баннера" />
|
<LlmSection title="Структура баннера" />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
||||||
|
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
||||||
import { NewsBlock } from "@/components/blocks/NewsBlock";
|
import { NewsBlock } from "@/components/blocks/NewsBlock";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -67,6 +68,17 @@ const LLM_NEWS_TEXT = `
|
|||||||
✕ Не добавлять описание/анонс в карточку
|
✕ Не добавлять описание/анонс в карточку
|
||||||
`.trim();
|
`.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 (
|
||||||
<div className="p-8 max-w-5xl mx-auto space-y-10">
|
<div className="p-8 max-w-5xl mx-auto space-y-10">
|
||||||
@@ -81,7 +93,7 @@ export default function NewsBlockPage() {
|
|||||||
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}>
|
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}>
|
||||||
Блок «Новости»
|
Блок «Новости»
|
||||||
</h1>
|
</h1>
|
||||||
<BlockMetaBar path="/blocks/news" defaultVersion="v1.1" defaultIsInPreview={false} />
|
<BlockMetaBar path="/blocks/news" defaultVersion="v1.1" defaultIsInPreview={false} defaultChangelog={CHANGELOG} />
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
Блок новостей с perm.oclinica.ru/lor — 4 карточки в ряд (дата + заголовок-ссылка),
|
Блок новостей с perm.oclinica.ru/lor — 4 карточки в ряд (дата + заголовок-ссылка),
|
||||||
кнопка «Все новости».
|
кнопка «Все новости».
|
||||||
@@ -132,20 +144,6 @@ export default function NewsBlockPage() {
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Changelog */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
История версий
|
|
||||||
</h2>
|
|
||||||
<div className="p-3 rounded-lg" style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}>
|
|
||||||
<p className="font-semibold text-xs mb-1" style={{ color: "var(--bb-text)" }}>v1.1 — 24.03.2026</p>
|
|
||||||
<ul className="list-disc list-inside space-y-0.5 text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
<li>H2: размер на <strong>36px</strong>, цвет на <strong>#000000</strong>, line-height 38px</li>
|
|
||||||
<li>Фон секции: с белого на <strong>#f2fee6</strong> (светло-зелёный, как на реальном сайте)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* LLM блок */}
|
{/* LLM блок */}
|
||||||
<LlmBlock path="/blocks/news" version="v1.1" specText={LLM_NEWS_TEXT}>
|
<LlmBlock path="/blocks/news" version="v1.1" specText={LLM_NEWS_TEXT}>
|
||||||
<LlmSection title="Структура карточки новости" />
|
<LlmSection title="Структура карточки новости" />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||||
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
||||||
|
import { type ChangelogEntry } from "@/components/ui/BlockChangelog";
|
||||||
import { ReviewsBlock } from "@/components/blocks/ReviewsBlock";
|
import { ReviewsBlock } from "@/components/blocks/ReviewsBlock";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -52,6 +53,16 @@ const LLM_REVIEWS_TEXT = `
|
|||||||
✕ Не убирать навигационные стрелки
|
✕ Не убирать навигационные стрелки
|
||||||
`.trim();
|
`.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 (
|
||||||
<div className="p-8 max-w-5xl mx-auto space-y-10">
|
<div className="p-8 max-w-5xl mx-auto space-y-10">
|
||||||
@@ -66,7 +77,7 @@ export default function ReviewsBlockPage() {
|
|||||||
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}>
|
<h1 className="text-2xl font-bold mb-2" style={{ color: "var(--bb-text)" }}>
|
||||||
Блок «Отзывы о нас»
|
Блок «Отзывы о нас»
|
||||||
</h1>
|
</h1>
|
||||||
<BlockMetaBar path="/blocks/reviews" defaultVersion="v1.1" defaultIsInPreview={false} />
|
<BlockMetaBar path="/blocks/reviews" defaultVersion="v1.1" defaultIsInPreview={false} defaultChangelog={CHANGELOG} />
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
Карусель отзывов с perm.oclinica.ru/lor — большая кавычка, текст, «Читать полностью», стрелки.
|
Карусель отзывов с perm.oclinica.ru/lor — большая кавычка, текст, «Читать полностью», стрелки.
|
||||||
</p>
|
</p>
|
||||||
@@ -157,19 +168,6 @@ export default function ReviewsBlockPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Changelog */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
История версий
|
|
||||||
</h2>
|
|
||||||
<div className="p-3 rounded-lg" style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}>
|
|
||||||
<p className="font-semibold text-xs mb-1" style={{ color: "var(--bb-text)" }}>v1.1 — 24.03.2026</p>
|
|
||||||
<ul className="list-disc list-inside space-y-0.5 text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
<li>H2: размер с ~20px на <strong>36px</strong>, цвет с #111827 на <strong>#000000</strong>, line-height 38px</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* LLM блок */}
|
{/* LLM блок */}
|
||||||
<LlmBlock path="/blocks/reviews" version="v1.1" specText={LLM_REVIEWS_TEXT}>
|
<LlmBlock path="/blocks/reviews" version="v1.1" specText={LLM_REVIEWS_TEXT}>
|
||||||
<LlmSection title="Структура блока" />
|
<LlmSection title="Структура блока" />
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
export interface ChangelogEntry {
|
||||||
|
version: string;
|
||||||
|
date: string;
|
||||||
|
changes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlockChangelogProps {
|
||||||
|
changelog: ChangelogEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BlockChangelog({ changelog }: BlockChangelogProps) {
|
||||||
|
if (!changelog || changelog.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||||
|
История версий
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-2 text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
{changelog.map((entry) => (
|
||||||
|
<div
|
||||||
|
key={entry.version}
|
||||||
|
className="p-3 rounded-lg"
|
||||||
|
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}
|
||||||
|
>
|
||||||
|
<p className="font-semibold text-xs mb-1" style={{ color: "var(--bb-text)" }}>
|
||||||
|
{entry.version} — {entry.date}
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc list-inside space-y-0.5 text-xs">
|
||||||
|
{entry.changes.map((change, i) => (
|
||||||
|
<li key={i}>{change}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { BlockChangelog, type ChangelogEntry } from "./BlockChangelog";
|
||||||
|
|
||||||
const LS_PREFIX = "bb-block-preview:";
|
const LS_PREFIX = "bb-block-preview:";
|
||||||
|
|
||||||
@@ -21,15 +22,17 @@ interface BlockMeta {
|
|||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
isInPreview: boolean;
|
isInPreview: boolean;
|
||||||
|
changelog: ChangelogEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BlockMetaBarProps {
|
interface BlockMetaBarProps {
|
||||||
path: string;
|
path: string;
|
||||||
defaultVersion: string;
|
defaultVersion: string;
|
||||||
defaultIsInPreview: boolean;
|
defaultIsInPreview: boolean;
|
||||||
|
defaultChangelog?: ChangelogEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: BlockMetaBarProps) {
|
export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview, defaultChangelog = [] }: 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);
|
||||||
@@ -55,7 +58,7 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: Block
|
|||||||
.catch(() => setApiDown(true));
|
.catch(() => setApiDown(true));
|
||||||
}, [apiUrl, path]);
|
}, [apiUrl, path]);
|
||||||
|
|
||||||
async function patch(data: { version?: string; isInPreview?: boolean }) {
|
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)}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
@@ -78,7 +81,6 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: Block
|
|||||||
|
|
||||||
async function togglePreview() {
|
async function togglePreview() {
|
||||||
if (apiDown) {
|
if (apiDown) {
|
||||||
// Fallback: toggle via localStorage
|
|
||||||
const newVal = !localPreview;
|
const newVal = !localPreview;
|
||||||
setLocalPreview(newVal);
|
setLocalPreview(newVal);
|
||||||
writeLocalPreview(path, newVal);
|
writeLocalPreview(path, newVal);
|
||||||
@@ -96,92 +98,100 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: Block
|
|||||||
|
|
||||||
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;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
className="flex items-center flex-wrap gap-3 px-4 py-2 rounded-lg text-xs mb-6"
|
<div
|
||||||
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}
|
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)" }}
|
||||||
{/* Version badge */}
|
>
|
||||||
<span className="font-semibold" style={{ color: "var(--bb-text-muted)" }}>
|
{/* Version badge */}
|
||||||
Версия:
|
<span className="font-semibold" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
</span>
|
Версия:
|
||||||
{editing ? (
|
|
||||||
<span className="flex items-center gap-1.5">
|
|
||||||
<input
|
|
||||||
className="px-2 py-0.5 rounded border text-xs font-mono w-20"
|
|
||||||
style={{ borderColor: "var(--bb-border)", color: "var(--bb-text)" }}
|
|
||||||
value={versionInput}
|
|
||||||
onChange={(e) => setVersionInput(e.target.value)}
|
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter') saveVersion(); if (e.key === 'Escape') setEditing(false); }}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={saveVersion}
|
|
||||||
disabled={saving}
|
|
||||||
className="px-2 py-0.5 rounded text-xs font-medium"
|
|
||||||
style={{ background: "var(--brand-053m)", color: "#fff" }}
|
|
||||||
>
|
|
||||||
{saving ? '...' : 'Сохранить'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => { setEditing(false); setVersionInput(version); }}
|
|
||||||
className="px-2 py-0.5 rounded text-xs"
|
|
||||||
style={{ color: "var(--bb-text-muted)" }}
|
|
||||||
>
|
|
||||||
Отмена
|
|
||||||
</button>
|
|
||||||
</span>
|
</span>
|
||||||
) : (
|
{editing ? (
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<input
|
||||||
|
className="px-2 py-0.5 rounded border text-xs font-mono w-20"
|
||||||
|
style={{ borderColor: "var(--bb-border)", color: "var(--bb-text)" }}
|
||||||
|
value={versionInput}
|
||||||
|
onChange={(e) => setVersionInput(e.target.value)}
|
||||||
|
onKeyDown={(e) => { if (e.key === 'Enter') saveVersion(); if (e.key === 'Escape') setEditing(false); }}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={saveVersion}
|
||||||
|
disabled={saving}
|
||||||
|
className="px-2 py-0.5 rounded text-xs font-medium"
|
||||||
|
style={{ background: "var(--brand-053m)", color: "#fff" }}
|
||||||
|
>
|
||||||
|
{saving ? '...' : 'Сохранить'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { setEditing(false); setVersionInput(version); }}
|
||||||
|
className="px-2 py-0.5 rounded text-xs"
|
||||||
|
style={{ color: "var(--bb-text-muted)" }}
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => !apiDown && setEditing(true)}
|
||||||
|
title={apiDown ? 'Версия из кода (API недоступен)' : 'Изменить версию'}
|
||||||
|
className="font-mono px-2 py-0.5 rounded"
|
||||||
|
style={{
|
||||||
|
background: "var(--bb-sidebar-active-bg, #dff0fa)",
|
||||||
|
color: "var(--brand-053m)",
|
||||||
|
cursor: apiDown ? 'default' : 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{version}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span style={{ color: "var(--bb-border)" }}>·</span>
|
||||||
|
|
||||||
|
{/* Preview toggle */}
|
||||||
<button
|
<button
|
||||||
onClick={() => !apiDown && setEditing(true)}
|
onClick={togglePreview}
|
||||||
title={apiDown ? 'API недоступен' : 'Изменить версию'}
|
disabled={togglingPreview || (!apiDown && !meta)}
|
||||||
className="font-mono px-2 py-0.5 rounded"
|
title={isInPreview ? "Убрать из превью страницы" : "Включить в превью страницы"}
|
||||||
style={{
|
className="flex items-center gap-1.5 px-2.5 py-1 rounded font-medium transition-colors cursor-pointer"
|
||||||
background: "var(--bb-sidebar-active-bg, #dff0fa)",
|
style={isInPreview ? {
|
||||||
color: "var(--brand-053m)",
|
background: "#dcfce7",
|
||||||
cursor: apiDown ? 'default' : 'pointer',
|
color: "#16a34a",
|
||||||
|
border: "1px solid #86efac",
|
||||||
|
} : {
|
||||||
|
background: "var(--bb-sidebar-bg)",
|
||||||
|
color: "var(--bb-text-muted)",
|
||||||
|
border: "1px solid var(--bb-border)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{version}
|
<span
|
||||||
|
className="inline-block w-1.5 h-1.5 rounded-full"
|
||||||
|
style={{ background: isInPreview ? "#22c55e" : "#d1d5db" }}
|
||||||
|
/>
|
||||||
|
{togglingPreview
|
||||||
|
? '...'
|
||||||
|
: isInPreview
|
||||||
|
? "В превью"
|
||||||
|
: "Не в превью"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
|
|
||||||
<span style={{ color: "var(--bb-border)" }}>·</span>
|
{/* Subtle offline dot instead of "API офлайн" text */}
|
||||||
|
{apiDown && (
|
||||||
|
<span
|
||||||
|
className="inline-block w-1.5 h-1.5 rounded-full"
|
||||||
|
title="API недоступен — данные из кода"
|
||||||
|
style={{ background: "#d1d5db" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Preview toggle */}
|
{/* Changelog rendered from API or fallback */}
|
||||||
<button
|
{changelog.length > 0 && <BlockChangelog changelog={changelog} />}
|
||||||
onClick={togglePreview}
|
</>
|
||||||
disabled={togglingPreview || (!apiDown && !meta)}
|
|
||||||
title={isInPreview ? "Убрать из превью страницы" : "Включить в превью страницы"}
|
|
||||||
className="flex items-center gap-1.5 px-2.5 py-1 rounded font-medium transition-colors cursor-pointer"
|
|
||||||
style={isInPreview ? {
|
|
||||||
background: "#dcfce7",
|
|
||||||
color: "#16a34a",
|
|
||||||
border: "1px solid #86efac",
|
|
||||||
} : {
|
|
||||||
background: "var(--bb-sidebar-bg)",
|
|
||||||
color: "var(--bb-text-muted)",
|
|
||||||
border: "1px solid var(--bb-border)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="inline-block w-1.5 h-1.5 rounded-full"
|
|
||||||
style={{ background: isInPreview ? "#22c55e" : "#d1d5db" }}
|
|
||||||
/>
|
|
||||||
{togglingPreview
|
|
||||||
? '...'
|
|
||||||
: isInPreview
|
|
||||||
? "В превью"
|
|
||||||
: "Не в превью"}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{apiDown && (
|
|
||||||
<>
|
|
||||||
<span style={{ color: "var(--bb-border)" }}>·</span>
|
|
||||||
<span style={{ color: "#f59e0b" }}>API офлайн</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-9
@@ -239,9 +239,17 @@
|
|||||||
- Фон новостей: #f2fee6 (ранее #fff)
|
- Фон новостей: #f2fee6 (ранее #fff)
|
||||||
- Типографика сайта: обновлена таблица стилей
|
- Типографика сайта: обновлена таблица стилей
|
||||||
- [x] FE: Добавлена «История версий» с changelog на каждую страницу блока
|
- [x] FE: Добавлена «История версий» с changelog на каждую страницу блока
|
||||||
|
- [x] BE: Метаданные блоков (version, isInPreview, changelog) перенесены в PostgreSQL
|
||||||
|
- Prisma schema: добавлено поле `changelog Json @default("[]")`
|
||||||
|
- Seed обновлён: 8 блоков с актуальными версиями и историей изменений
|
||||||
|
- API: PATCH /blocks/by-path принимает changelog
|
||||||
|
- CORS: открыт для любого localhost порта
|
||||||
|
- BlockMetaBar: загружает version + changelog из API, fallback на defaultVersion/defaultChangelog
|
||||||
|
- Компонент BlockChangelog: отображает историю версий из API или из кода
|
||||||
|
- Надпись «API офлайн» заменена на серую точку
|
||||||
- [ ] FE: Footer
|
- [ ] FE: Footer
|
||||||
|
|
||||||
**Результат спринта:** Hero v1.2, Doctors v1.2, Reviews v1.1, ContactForms v1.1, News v1.1 — все стили синхронизированы с реальным сайтом.
|
**Результат спринта:** Hero v1.2, Doctors v1.2, Reviews v1.1, ContactForms v1.1, News v1.1 — стили синхронизированы с реальным сайтом. Метаданные блоков хранятся в БД.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -285,14 +293,17 @@
|
|||||||
|
|
||||||
### Задачи
|
### Задачи
|
||||||
|
|
||||||
- [ ] FE: Страница `/pages/preview` — пустое состояние с кнопкой «Создать»
|
- [x] FE: Страница `/pages/preview` — пустое состояние с кнопкой «Создать»
|
||||||
- [ ] FE: Логика `localStorage` — сохранение/сброс состояния превью
|
- [x] FE: Логика `localStorage` — сохранение/сброс состояния превью
|
||||||
- [ ] FE: Рефактор `/blocks/hero/page.tsx` — вынести баннер в компонент `HeroBlock` (переиспользуемый)
|
- [x] FE: Рефактор `/blocks/hero/page.tsx` — вынести баннер в компонент `HeroBlock` (переиспользуемый)
|
||||||
- [ ] FE: Рефактор `/blocks/doctors/page.tsx` — вынести в компонент `DoctorsBlock`
|
- [x] FE: Рефактор `/blocks/doctors/page.tsx` — вынести в компонент `DoctorsBlock`
|
||||||
- [ ] FE: Placeholder-компонент для блоков, которые ещё не готовы (серая рамка с названием блока)
|
- [x] FE: Placeholder-компонент для блоков, которые ещё не готовы (серая рамка с названием блока)
|
||||||
- [ ] FE: Сборка превью: рендер всех доступных компонентов в порядке реального сайта
|
- [x] FE: Сборка превью: рендер всех доступных компонентов в порядке реального сайта
|
||||||
- [ ] FE: Sidebar — добавить «Просмотр страницы» в раздел «Страницы»
|
- [x] FE: Sidebar — добавить «Просмотр страницы» в раздел «Страницы»
|
||||||
- [ ] FE: Кнопка «Пересобрать» в созданном состоянии
|
- [x] FE: Кнопка «Пересобрать» в созданном состоянии
|
||||||
|
- [x] FE: Toggle «В превью» через localStorage (fallback при API офлайн)
|
||||||
|
- [x] BE: BlockMetaBar + PreviewClient подключены к NestJS API `/blocks`
|
||||||
|
- [x] BE: Метаданные блоков (version, changelog, isInPreview) в PostgreSQL
|
||||||
- [ ] Docs: Добавить `/pages/preview` v1.0 в LLM_CONTEXT.md
|
- [ ] Docs: Добавить `/pages/preview` v1.0 в LLM_CONTEXT.md
|
||||||
|
|
||||||
### Зависимости
|
### Зависимости
|
||||||
|
|||||||
Reference in New Issue
Block a user