Compare commits
12 Commits
sprint/5
...
eda348deea
| Author | SHA1 | Date | |
|---|---|---|---|
| eda348deea | |||
| 1b64ccaa0f | |||
| 5b54ad5c23 | |||
| 196526ffc4 | |||
| 3094e9a2b7 | |||
| e20d222183 | |||
| 36d5faf67d | |||
| 950799193a | |||
| 2ed7eee63d | |||
| c8217cfc2f | |||
| be37ae2508 | |||
| 72829b5d46 |
@@ -29,3 +29,4 @@ prisma/migrations/*.sql.bak
|
|||||||
|
|
||||||
# Claude
|
# Claude
|
||||||
.claude/
|
.claude/
|
||||||
|
.vercel
|
||||||
|
|||||||
@@ -24,8 +24,11 @@
|
|||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@prisma/adapter-pg": "^7.5.0",
|
||||||
"@prisma/client": "^7.5.0",
|
"@prisma/client": "^7.5.0",
|
||||||
|
"@types/pg": "^8.20.0",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
|
"pg": "^8.20.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
@@ -52,9 +55,13 @@
|
|||||||
"ts-loader": "^9.5.2",
|
"ts-loader": "^9.5.2",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"typescript-eslint": "^8.20.0"
|
"typescript-eslint": "^8.20.0"
|
||||||
},
|
},
|
||||||
|
"prisma": {
|
||||||
|
"seed": "ts-node prisma/seed.ts"
|
||||||
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
"js",
|
"js",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export default defineConfig({
|
|||||||
schema: "prisma/schema.prisma",
|
schema: "prisma/schema.prisma",
|
||||||
migrations: {
|
migrations: {
|
||||||
path: "prisma/migrations",
|
path: "prisma/migrations",
|
||||||
|
seed: "tsx prisma/seed.ts",
|
||||||
},
|
},
|
||||||
datasource: {
|
datasource: {
|
||||||
url: process.env["DATABASE_URL"],
|
url: process.env["DATABASE_URL"],
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "Role" AS ENUM ('viewer', 'editor');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "ComponentStatus" AS ENUM ('draft', 'review', 'approved');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"passwordHash" TEXT NOT NULL,
|
||||||
|
"role" "Role" NOT NULL DEFAULT 'viewer',
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ExperimentalComponent" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"baseComponent" TEXT NOT NULL,
|
||||||
|
"attributes" JSONB NOT NULL,
|
||||||
|
"status" "ComponentStatus" NOT NULL DEFAULT 'draft',
|
||||||
|
"authorId" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ExperimentalComponent_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Block" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"path" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"version" TEXT NOT NULL,
|
||||||
|
"isInPreview" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "Block_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Block_path_key" ON "Block"("path");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ExperimentalComponent" ADD CONSTRAINT "ExperimentalComponent_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Block" ADD COLUMN "changelog" JSONB NOT NULL DEFAULT '[]';
|
||||||
@@ -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");
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (e.g., Git)
|
||||||
|
provider = "postgresql"
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client"
|
provider = "prisma-client-js"
|
||||||
output = "../generated/prisma"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
@@ -40,3 +39,27 @@ model ExperimentalComponent {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Block {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
path String @unique
|
||||||
|
name String
|
||||||
|
version String
|
||||||
|
isInPreview Boolean @default(false)
|
||||||
|
changelog Json @default("[]")
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { PrismaPg } from '@prisma/adapter-pg';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||||
|
const prisma = new PrismaClient({ adapter });
|
||||||
|
|
||||||
|
const BLOCKS = [
|
||||||
|
{
|
||||||
|
path: '/blocks/navigation',
|
||||||
|
name: 'Шапка / Навигация',
|
||||||
|
version: 'v1.3',
|
||||||
|
isInPreview: true,
|
||||||
|
changelog: [
|
||||||
|
{ version: 'v1.3', date: '24.03.2026', changes: [
|
||||||
|
'Подменю: выпадающие списки при hover',
|
||||||
|
'Hover-эффект: бежевый фон #f5f0e6',
|
||||||
|
'Пункты подчёркнуты, без разделителей',
|
||||||
|
'Все пункты чёрного цвета #000',
|
||||||
|
]},
|
||||||
|
{ version: 'v1.2', date: '24.03.2026', changes: [
|
||||||
|
'Убрана рамка и тень вокруг шапки',
|
||||||
|
'3 столбца: логотип | ссылки | телефон+кнопка',
|
||||||
|
'Реальный логотип logo-main.png',
|
||||||
|
]},
|
||||||
|
{ version: 'v1.1', date: '24.03.2026', changes: [
|
||||||
|
'Адрес: «К. Цеткин, 9», ссылки, телефон 25px, меню 18px',
|
||||||
|
]},
|
||||||
|
{ version: 'v1.0', date: '23.03.2026', changes: ['Топ-бар, логотип, главное меню из 8 пунктов'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/blocks/hero',
|
||||||
|
name: 'Hero-баннер',
|
||||||
|
version: 'v1.3',
|
||||||
|
isInPreview: true,
|
||||||
|
changelog: [
|
||||||
|
{ version: 'v1.3', date: '24.03.2026', changes: [
|
||||||
|
'Счётчик: «Поделиться ✉ 98572» (было «👁 98 573 просмотра»)',
|
||||||
|
'Убраны кнопки VK/FB/TW',
|
||||||
|
]},
|
||||||
|
{ 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.1',
|
||||||
|
isInPreview: false,
|
||||||
|
changelog: [
|
||||||
|
{ version: 'v1.1', date: '24.03.2026', changes: [
|
||||||
|
'Адрес: «ул. Цитная, 9» → «ул. Клары Цеткин, 9»',
|
||||||
|
'Цвет ссылок: #52b4bd (было #0089c3)',
|
||||||
|
]},
|
||||||
|
{ 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.2',
|
||||||
|
isInPreview: true,
|
||||||
|
changelog: [
|
||||||
|
{ version: 'v1.2', date: '24.03.2026', changes: [
|
||||||
|
'Кнопка: bb-btn-lg 18px bold (было bb-btn-md 14px)',
|
||||||
|
'border-radius кнопки: 4px (было 7px)',
|
||||||
|
]},
|
||||||
|
{ 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.1',
|
||||||
|
isInPreview: false,
|
||||||
|
changelog: [
|
||||||
|
{ version: 'v1.1', date: '24.03.2026', changes: [
|
||||||
|
'Колонка «О клинике»: 13 ссылок по реальному сайту (было 7)',
|
||||||
|
'Колонка «Заболевания»: 5 категорий (было 6 конкретных диагнозов)',
|
||||||
|
'Колонка «Вопрос-ответ»: 6 пунктов по реальному сайту',
|
||||||
|
'Колонка «Операции»: 11 операций (было 6)',
|
||||||
|
'Два адреса: Клары Цеткин 9 + Газеты Звезда 31А',
|
||||||
|
'Два графика работы по филиалам',
|
||||||
|
'Соцсети: добавлен Дзен',
|
||||||
|
]},
|
||||||
|
{ version: 'v1.0', date: '23.03.2026', changes: ['4 колонки ссылок, адрес, часы работы'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
for (const block of BLOCKS) {
|
||||||
|
await prisma.block.upsert({
|
||||||
|
where: { path: block.path },
|
||||||
|
update: { version: block.version, isInPreview: block.isInPreview, changelog: block.changelog },
|
||||||
|
create: block,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(`Seeded ${BLOCKS.length} blocks`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(() => prisma.$disconnect());
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
import { BlocksModule } from './blocks/blocks.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [BlocksModule],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Controller, Get, Post, Patch, Param, Query, Body } from '@nestjs/common';
|
||||||
|
import { BlocksService } from './blocks.service';
|
||||||
|
|
||||||
|
@Controller('blocks')
|
||||||
|
export class BlocksController {
|
||||||
|
constructor(private blocks: BlocksService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
findAll() {
|
||||||
|
return this.blocks.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('by-path')
|
||||||
|
findByPath(@Query('path') path: string) {
|
||||||
|
return this.blocks.findByPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('by-path')
|
||||||
|
update(
|
||||||
|
@Query('path') path: string,
|
||||||
|
@Body() body: { version?: string; isInPreview?: boolean; changelog?: object[] },
|
||||||
|
) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { BlocksController } from './blocks.controller';
|
||||||
|
import { BlocksService } from './blocks.service';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [BlocksController],
|
||||||
|
providers: [BlocksService, PrismaService],
|
||||||
|
})
|
||||||
|
export class BlocksModule {}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BlocksService {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
findAll() {
|
||||||
|
return this.prisma.block.findMany({ orderBy: { createdAt: 'asc' } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByPath(path: string) {
|
||||||
|
const existing = await this.prisma.block.findUnique({ where: { path } });
|
||||||
|
if (existing) return existing;
|
||||||
|
return this.prisma.block.create({
|
||||||
|
data: { path, name: path, version: 'v0.1', isInPreview: false },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
update(path: string, data: { version?: string; isInPreview?: boolean; changelog?: object[] }) {
|
||||||
|
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 } });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
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:\d+$/] });
|
||||||
await app.listen(process.env.PORT ?? 3000);
|
await app.listen(process.env.PORT ?? 3000);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import { PrismaPg } from '@prisma/adapter-pg';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||||
|
constructor() {
|
||||||
|
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||||
|
super({ adapter });
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.$connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
await this.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
"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.2",
|
||||||
|
date: "25.03.2026",
|
||||||
|
changes: [
|
||||||
|
"Убраны горизонтальные отступы: padding 40px 48px → 40px 0 (как на реальном сайте)",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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.2"
|
||||||
|
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.2" 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,224 +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";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Вводный текст (CEO-блок). Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
title: "Вводный текст (CEO-блок). Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
};
|
};
|
||||||
|
|
||||||
const LLM_CEO_TEXT = `
|
|
||||||
БЛОК: Вводный текст клиники (CEO-блок)
|
|
||||||
Источник: perm.oclinica.ru/lor — секция после баннера
|
|
||||||
Версия: v1.0
|
|
||||||
|
|
||||||
НАЗНАЧЕНИЕ:
|
|
||||||
Информационный блок под 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 CEO_QUESTIONS = [
|
|
||||||
"У вас болит ухо, заложен нос, першит в горле, и вы не можете понять причину?",
|
|
||||||
"Вам срочно нужен платный ЛОР в Перми или, как ещё говорят, «ухогорлонос»?",
|
|
||||||
"Заболел ребёнок?",
|
|
||||||
"Срочно ищете частные ЛОР-клиники Перми для детей 0+ и взрослых с удобным режимом работы с 9:00 до 21:00 по будням?",
|
|
||||||
"Вам назначили проведение эндоскопической операции на ухе, горле или носе?",
|
|
||||||
];
|
|
||||||
|
|
||||||
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>
|
|
||||||
<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 p-8"
|
|
||||||
style={{
|
|
||||||
background: "#fff",
|
|
||||||
border: "1px solid var(--bb-border)",
|
|
||||||
fontFamily: "var(--font-web)",
|
|
||||||
fontSize: 14,
|
|
||||||
lineHeight: 1.75,
|
|
||||||
color: "#374151",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Клиника ухо, нос специализируется на оториноларингологии – лечении взрослых и детей
|
|
||||||
с ЛОР заболеваниями. ЛОР клиника представлена на двух адресах:{" "}
|
|
||||||
<a href="#" style={{ color: "#0089c3" }}>ул. Цитная, 9</a>, <a href="#" style={{ color: "#0089c3" }}>ул. Г. Звезда, 31а</a>.{" "}
|
|
||||||
Это <a href="#" style={{ color: "#0089c3" }}>Клиника лечения кашля и аллергии</a>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-4 space-y-2">
|
|
||||||
{CEO_QUESTIONS.map((q) => (
|
|
||||||
<p key={q}>— {q}</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="mt-4">
|
|
||||||
Обращайтесь в ЛОР центр ухо, горло, нос в Перми, наши врачи оториноларингологи обязательно Вам помогут!
|
|
||||||
</p>
|
|
||||||
<p className="mt-2">
|
|
||||||
Клиника ЛОР болезней ухо, горло, нос – это наиболее современный центр оториноларингологии в Перми,
|
|
||||||
благодаря эндоскопическому оборудованию, высокому профессионализму оториноларингологов.
|
|
||||||
</p>
|
|
||||||
<p className="mt-4 font-medium">Будьте здоровы!</p>
|
|
||||||
</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.0" 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,236 +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";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Формы записи. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
title: "Формы записи. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
};
|
};
|
||||||
|
|
||||||
const LLM_FORMS_TEXT = `
|
|
||||||
БЛОК: Формы записи и обратной связи
|
|
||||||
Источник: perm.oclinica.ru/lor — два блока форм на одной странице
|
|
||||||
Версия: v1.0
|
|
||||||
|
|
||||||
ФОРМА 1: «Будьте здоровы!» (форма записи на приём)
|
|
||||||
Позиция: после блока отзывов
|
|
||||||
Заголовок H2: «Будьте здоровы!»
|
|
||||||
Подзаголовок: «Запишитесь на приём к врачу!»
|
|
||||||
Фон секции: #b8e6ed (светло-бирюзовый)
|
|
||||||
Поля формы:
|
|
||||||
1. Текстовый input: placeholder «Введите ваше имя» (height 50px)
|
|
||||||
2. Телефонный input: placeholder «Введите ваш телефон» (height 50px)
|
|
||||||
3. Select: «Выберите ЛОР врача» (height 50px)
|
|
||||||
4. Checkbox: «Отправляя данные, я даю согласие на обработку персональных данных»
|
|
||||||
Кнопка: «Запишите меня!» — стиль bb-btn-primary (#FFA39C)
|
|
||||||
Ширина формы: ~400px центрирована или в колонку
|
|
||||||
|
|
||||||
ФОРМА 2: «Узнайте стоимость операции» (консультация)
|
|
||||||
Позиция: после блока новостей
|
|
||||||
Заголовок H2: «Узнайте стоимость операции»
|
|
||||||
Подзаголовок: «Проконсультируйтесь с ассистентом хирурга»
|
|
||||||
Фон секции: #ffffff
|
|
||||||
Поля формы:
|
|
||||||
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)
|
|
||||||
— Форма 1 на бирюзовом фоне (#b8e6ed), форма 2 на белом
|
|
||||||
|
|
||||||
ПРАВИЛА:
|
|
||||||
✓ Чекбокс согласия обязателен в каждой форме
|
|
||||||
✓ Кнопки отправки: bb-btn-primary (#FFA39C)
|
|
||||||
✓ Форма 1 на фоне #b8e6ed
|
|
||||||
✕ Не менять порядок полей
|
|
||||||
✕ Не убирать чекбокс согласия
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
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>
|
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
Два блока форм с perm.oclinica.ru/lor — запись на приём и запрос стоимости операции.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Форма 1: Будьте здоровы */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Форма 1 — «Будьте здоровы!»
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
Фон секции: #b8e6ed. Запись на приём к врачу.
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
className="rounded-xl py-10 px-8 flex flex-col items-center gap-6"
|
|
||||||
style={{ background: "#b8e6ed" }}
|
|
||||||
>
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-2xl font-bold mb-1" style={{ color: "#111827" }}>
|
|
||||||
Будьте здоровы!
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm" style={{ color: "#374151" }}>
|
|
||||||
Запишитесь на приём к врачу!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="w-full max-w-sm space-y-3">
|
|
||||||
<input
|
|
||||||
className="bb-input"
|
|
||||||
type="text"
|
|
||||||
placeholder="Введите ваше имя"
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
className="bb-input"
|
|
||||||
type="tel"
|
|
||||||
placeholder="Введите ваш телефон"
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
<select className="bb-select" disabled>
|
|
||||||
<option>Выберите ЛОР врача</option>
|
|
||||||
</select>
|
|
||||||
<label className="flex items-start gap-2 text-xs cursor-pointer" style={{ color: "#374151" }}>
|
|
||||||
<input type="checkbox" className="bb-checkbox mt-0.5" readOnly />
|
|
||||||
<span>
|
|
||||||
Отправляя данные, я даю согласие на обработку персональных данных
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<button className="bb-btn bb-btn-md bb-btn-primary w-full">
|
|
||||||
Запишите меня!
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Форма 2: Узнайте стоимость */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Форма 2 — «Узнайте стоимость операции»
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
Фон секции: #ffffff. Запрос консультации по стоимости.
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
className="rounded-xl py-10 px-8 flex flex-col items-center gap-6"
|
|
||||||
style={{ background: "#fff", border: "1px solid var(--bb-border)" }}
|
|
||||||
>
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-2xl font-bold mb-1" style={{ color: "#111827" }}>
|
|
||||||
Узнайте стоимость операции
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm" style={{ color: "#374151" }}>
|
|
||||||
Проконсультируйтесь с ассистентом хирурга
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="w-full max-w-sm space-y-3">
|
|
||||||
<input
|
|
||||||
className="bb-input"
|
|
||||||
type="text"
|
|
||||||
placeholder="Введите ваше имя"
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
className="bb-input"
|
|
||||||
type="tel"
|
|
||||||
placeholder="Введите ваш телефон"
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
<label className="flex items-start gap-2 text-xs cursor-pointer" style={{ color: "#374151" }}>
|
|
||||||
<input type="checkbox" className="bb-checkbox mt-0.5" readOnly />
|
|
||||||
<span>
|
|
||||||
Отправляя данные, я даю согласие на обработку персональных данных
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<button className="bb-btn bb-btn-md bb-btn-primary w-full">
|
|
||||||
Перезвоните мне
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
{[
|
|
||||||
["Заголовок", "«Будьте здоровы!»", "«Узнайте стоимость операции»"],
|
|
||||||
["Подзаголовок", "«Запишитесь на приём к врачу!»", "«Проконсультируйтесь с ассистентом хирурга»"],
|
|
||||||
["Фон секции", "#b8e6ed", "#ffffff"],
|
|
||||||
["Поля", "Имя + Телефон + 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.0" 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: "Форма 1: фон #b8e6ed, форма 2: фон #fff" },
|
|
||||||
{ 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,257 +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";
|
||||||
|
|
||||||
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.0
|
|
||||||
|
|
||||||
СТРУКТУРА ПОДВАЛА:
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
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>
|
|
||||||
<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>
|
|
||||||
<div
|
|
||||||
className="rounded-xl overflow-hidden"
|
|
||||||
style={{ border: "1px solid var(--bb-border)" }}
|
|
||||||
>
|
|
||||||
{/* 4 колонки ссылок */}
|
|
||||||
<div
|
|
||||||
className="grid grid-cols-4 gap-6 p-8"
|
|
||||||
style={{ background: "#f8f9fa", borderBottom: "1px solid #e5e7eb" }}
|
|
||||||
>
|
|
||||||
{FOOTER_COLUMNS.map((col) => (
|
|
||||||
<div key={col.title}>
|
|
||||||
<p className="font-semibold text-sm mb-3" style={{ color: "#111827" }}>
|
|
||||||
{col.title}
|
|
||||||
</p>
|
|
||||||
<ul className="space-y-1.5">
|
|
||||||
{col.links.map((link) => (
|
|
||||||
<li key={link}>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="text-xs bb-nav-link"
|
|
||||||
style={{ color: "#53514e", textDecoration: "none" }}
|
|
||||||
>
|
|
||||||
{link}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Нижняя полоса */}
|
|
||||||
<div
|
|
||||||
className="flex items-start justify-between gap-6 px-8 py-5"
|
|
||||||
style={{ background: "#fff" }}
|
|
||||||
>
|
|
||||||
{/* Логотип */}
|
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
|
||||||
<div
|
|
||||||
className="w-10 h-10 rounded-full flex items-center justify-center text-white font-bold"
|
|
||||||
style={{ background: "#0089c3" }}
|
|
||||||
>
|
|
||||||
✚
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className="text-[10px] font-bold uppercase leading-tight"
|
|
||||||
style={{ color: "#53514e" }}
|
|
||||||
>
|
|
||||||
Клиника<br />ухо, горло, нос
|
|
||||||
</div>
|
|
||||||
<div className="text-[8px] leading-tight" style={{ color: "#9ca3af" }}>
|
|
||||||
им. проф. Е.Н.Оленевой
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Адрес и соцсети */}
|
|
||||||
<div className="text-center space-y-2">
|
|
||||||
<p className="text-xs" style={{ color: "#374151" }}>
|
|
||||||
Мы находимся по адресу: Пермь, ул. Г. Звезда, 31а
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
|
||||||
{["VK", "OK", "YT", "TG"].map((s) => (
|
|
||||||
<a
|
|
||||||
key={s}
|
|
||||||
href="#"
|
|
||||||
className="text-[11px] px-2 py-1 rounded"
|
|
||||||
style={{
|
|
||||||
background: "var(--bb-sidebar-bg)",
|
|
||||||
border: "1px solid var(--bb-border)",
|
|
||||||
color: "var(--bb-text-muted)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{s}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Часы работы */}
|
|
||||||
<div className="text-right text-xs space-y-1" style={{ color: "#374151" }}>
|
|
||||||
<p className="font-semibold text-xs mb-1" style={{ color: "#111827" }}>
|
|
||||||
Часы работы:
|
|
||||||
</p>
|
|
||||||
<p>Пн–пт: 9:00–21:00</p>
|
|
||||||
<p>Сб: 9:00–18:00</p>
|
|
||||||
<p>Вс: выходной</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-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.0" 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,238 +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";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Блок «Наши врачи». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
title: "Блок «Наши врачи». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATS = [
|
|
||||||
{ num: "27", label: "ЛОР врачей работает в клинике", prefix: "Ежедневно" },
|
|
||||||
{ num: "6", label: "кандидатов медицинских наук", prefix: "В том числе" },
|
|
||||||
{ num: "12 000+", label: "успешно проведённых операций", prefix: "Свыше" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const DOCTORS = [
|
|
||||||
{
|
|
||||||
name: "Макарова Людмила Германовна",
|
|
||||||
spec: "ЛОР врач, сурдолог",
|
|
||||||
photo: "/doctors/makarova.jpg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Семерикова Наталия Александровна",
|
|
||||||
spec: "ЛОР врач, сурдолог, хирург. К.М.Н. Завед. Центром сурдологии",
|
|
||||||
photo: "/doctors/semerikova.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Ворончихина Наталия Валерьевна",
|
|
||||||
spec: "Отоневролог, хирург. К.М.Н., доцент кафедры ПГМУ",
|
|
||||||
photo: "/doctors/voronchikhina.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Лобанова Ирина Юрьевна",
|
|
||||||
spec: "ЛОР врач, сурдолог",
|
|
||||||
photo: "/doctors/lobanova.jpg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Торсунова Наталья Сергеевна",
|
|
||||||
spec: "Специалист по слухопротезированию (сурдоакустик)",
|
|
||||||
photo: "/doctors/torsunova.jpg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Суворова Светлана Викторовна",
|
|
||||||
spec: "ЛОР врач, сурдолог",
|
|
||||||
photo: "/doctors/suvorova.jpg",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const LLM_DOCTORS_TEXT = `
|
|
||||||
БЛОК: Наши врачи
|
|
||||||
Источник: perm.oclinica.ru/lor — блок под CEO-текстом
|
|
||||||
Версия: v1.0
|
|
||||||
|
|
||||||
СТРУКТУРА БЛОКА:
|
|
||||||
1. ЗАГОЛОВОК H2: «Приём ведут опытные ЛОР врачи»
|
|
||||||
Подзаголовок: описание принципа работы врачей клиники
|
|
||||||
|
|
||||||
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: #111827, ~30px (text-3xl)
|
|
||||||
Статистика текст: #60959c (серо-бирюзовый)
|
|
||||||
Статистика черта: border-bottom 3px solid #60959c
|
|
||||||
Имя врача: #60959c
|
|
||||||
Специализация: #374151
|
|
||||||
|
|
||||||
ПРАВИЛА:
|
|
||||||
✓ Заголовок H2 + описание обязательны
|
|
||||||
✓ 3 stat-блока в ряд
|
|
||||||
✓ Сетка 3 колонки, 2 ряда (6 карточек)
|
|
||||||
✕ Не отображать более 6 врачей в основном блоке
|
|
||||||
✕ Не убирать статистику
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
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>
|
|
||||||
<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 space-y-8"
|
|
||||||
style={{ background: "#fff", border: "1px solid var(--bb-border)" }}
|
|
||||||
>
|
|
||||||
{/* Заголовок */}
|
|
||||||
<div>
|
|
||||||
<h2 className="text-3xl font-bold mb-3" style={{ color: "#111827" }}>
|
|
||||||
Приём ведут опытные ЛОР врачи
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm" style={{ color: "#374151", lineHeight: 1.7 }}>
|
|
||||||
Фундаментальная теоретическая подготовка и большой практический опыт в сочетании
|
|
||||||
с внимательным индивидуальным подходом являются причиной успеха лечения тысяч наших пациентов
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Статистика — текст тёмно-бирюзовым, без фоновых блоков */}
|
|
||||||
<div className="grid grid-cols-3 gap-6">
|
|
||||||
{STATS.map((s) => (
|
|
||||||
<div key={s.num} className="pb-3" style={{ borderBottom: "3px solid #60959c" }}>
|
|
||||||
<p className="text-lg font-bold leading-snug" style={{ color: "#60959c" }}>
|
|
||||||
{s.prefix} {s.num} {s.label}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Сетка врачей — плотнее, имена тёмно-бирюзовым */}
|
|
||||||
<div className="grid grid-cols-6 gap-3">
|
|
||||||
{DOCTORS.map((doc) => (
|
|
||||||
<div key={doc.name} className="flex flex-col items-center text-center gap-1.5">
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
<img
|
|
||||||
src={doc.photo}
|
|
||||||
alt={doc.name}
|
|
||||||
style={{
|
|
||||||
width: 110,
|
|
||||||
height: 150,
|
|
||||||
objectFit: "cover",
|
|
||||||
objectPosition: "center top",
|
|
||||||
borderRadius: 4,
|
|
||||||
display: "block",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p
|
|
||||||
className="text-xs font-medium leading-snug"
|
|
||||||
style={{ color: "#60959c" }}
|
|
||||||
>
|
|
||||||
{doc.name}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className="text-[11px] mt-0.5 leading-snug"
|
|
||||||
style={{ color: "#374151" }}
|
|
||||||
>
|
|
||||||
{doc.spec}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</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.1" specText={LLM_DOCTORS_TEXT}>
|
|
||||||
<LlmSection title="Структура блока" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Элемент", "Содержимое", "Стиль"]}
|
|
||||||
rows={[
|
|
||||||
["H2", "«Приём ведут опытные ЛОР врачи»", "~30px (text-3xl), font-bold, #111827"],
|
|
||||||
["Подзаголовок", "Описание принципа работы", "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={[
|
|
||||||
["Фото", "88×120px (в блоке) или 110×160px (на странице врачей), object-fit: cover"],
|
|
||||||
["Имя", "12–14px, font-weight 500, #111827"],
|
|
||||||
["Специализация", "11–12px, #6b7280"],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Правила" />
|
|
||||||
<LlmRules
|
|
||||||
rules={[
|
|
||||||
{ ok: true, text: "H2 + описание обязательны" },
|
|
||||||
{ ok: true, text: "3 stat-блока в ряд, фон #dff0fa" },
|
|
||||||
{ ok: true, text: "Сетка 6 карточек (3 колонки)" },
|
|
||||||
{ 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,286 +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";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Hero-баннер. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
title: "Hero-баннер. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
};
|
};
|
||||||
|
|
||||||
const LLM_HERO_TEXT = `
|
|
||||||
БЛОК: Hero-баннер (главный баннер страницы)
|
|
||||||
Источник: perm.oclinica.ru/lor — реальный баннер раздела ЛОР
|
|
||||||
Версия: v1.1
|
|
||||||
|
|
||||||
ЗАГОЛОВОК СТРАНИЦЫ (H1, над баннером):
|
|
||||||
«ЛОР Клиника ухо, горло, нос – медицинский центр лечения ЛОР заболеваний у детей и взрослых»
|
|
||||||
Шрифт: Fira Sans, ~26–28px, weight 700, цвет: #53514e (--brand-073m)
|
|
||||||
|
|
||||||
СТРУКТУРА БАННЕРА (двухколоночная, единый фон #f9f4e7):
|
|
||||||
Левая колонка (~50%):
|
|
||||||
— Заголовок: «ЭНДОСКОПИЧЕСКОЕ ХИРУРГИЧЕСКОЕ ЛЕЧЕНИЕ ЛОР ОРГАНОВ»
|
|
||||||
Шрифт: Fira Sans, ~18px, weight 700, uppercase, цвет #111827
|
|
||||||
— 3 пункта с галочками (✓ зелёный):
|
|
||||||
1. «БЕЗОПАСНО – оперируют хирурги с 15-летним опытом работы»
|
|
||||||
2. «БЕЗ ВНЕШНИХ РАЗРЕЗОВ – хирургия сверхмалых размеров»
|
|
||||||
3. «БЫСТРО – под наблюдением врача пациент находится 1 сутки»
|
|
||||||
Ключевое слово: uppercase bold; описание: обычный текст, ~13px
|
|
||||||
— Кнопка «Узнать стоимость операции» — стиль: bb-btn-outline (светлая, без заливки)
|
|
||||||
Правая колонка (~50%):
|
|
||||||
— Фото врача с пациентом
|
|
||||||
— Изображение занимает всю высоту блока
|
|
||||||
|
|
||||||
ПОД БАННЕРОМ:
|
|
||||||
— Кнопки соцсетей (Facebook, VK, Twitter/X), цвет #6b7280
|
|
||||||
— Счётчик просмотров
|
|
||||||
|
|
||||||
ЦВЕТА:
|
|
||||||
Фон баннера: #f9f4e7 (светло-кремовый, единый для всего блока)
|
|
||||||
Кнопка CTA: outline-стиль (светлая), не коралловая
|
|
||||||
Заголовок блока: #111827
|
|
||||||
Пункты: ключевое слово #111827 bold, описание #374151
|
|
||||||
Галочка: цвет бренда (бежевый/золотой ~#bf9975)
|
|
||||||
|
|
||||||
ПРАВИЛА:
|
|
||||||
✓ Фон баннера всегда #f9f4e7 (светло-кремовый) — единый, без разделения на зоны
|
|
||||||
✓ Заголовок блока uppercase, жирный
|
|
||||||
✓ Три пункта с галочками обязательны
|
|
||||||
✕ Не менять фон баннера на другой цвет
|
|
||||||
✕ Не разбивать фон на два разных цвета
|
|
||||||
✕ Не убирать три пункта с галочками
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const HERO_CHECKS = [
|
|
||||||
{
|
|
||||||
key: "БЕЗОПАСНО",
|
|
||||||
desc: "оперируют хирурги с 15-летним опытом работы",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "БЕЗ ВНЕШНИХ РАЗРЕЗОВ",
|
|
||||||
desc: "хирургия смяткими размерами",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "БЫСТРО",
|
|
||||||
desc: "под наблюдением врача пациент находится 1 сутки",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
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>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* H1 страницы */}
|
|
||||||
<h2
|
|
||||||
className="text-xl font-bold leading-snug"
|
|
||||||
style={{ color: "#53514e" }}
|
|
||||||
>
|
|
||||||
ЛОР Клиника ухо, горло, нос – медицинский центр лечения ЛОР заболеваний у детей и взрослых
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{/* Баннер — единый светло-кремовый фон */}
|
|
||||||
<div
|
|
||||||
className="rounded-xl overflow-hidden flex"
|
|
||||||
style={{ background: "#f9f4e7", minHeight: 280 }}
|
|
||||||
>
|
|
||||||
{/* Левая часть — контент на кремовом фоне */}
|
|
||||||
<div
|
|
||||||
className="flex flex-col justify-center gap-5 p-8"
|
|
||||||
style={{ width: "50%", flexShrink: 0 }}
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
className="text-base font-bold uppercase leading-snug"
|
|
||||||
style={{ color: "#111827" }}
|
|
||||||
>
|
|
||||||
Эндоскопическое хирургическое лечение ЛОР органов
|
|
||||||
</p>
|
|
||||||
<ul className="space-y-3">
|
|
||||||
{HERO_CHECKS.map((c) => (
|
|
||||||
<li key={c.key} className="flex items-start gap-2 text-sm">
|
|
||||||
<span
|
|
||||||
className="shrink-0 font-bold"
|
|
||||||
style={{ color: "#bf9975", marginTop: 1 }}
|
|
||||||
>
|
|
||||||
✓
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<span className="font-bold uppercase" style={{ color: "#111827" }}>
|
|
||||||
{c.key}
|
|
||||||
</span>{" "}
|
|
||||||
<span style={{ color: "#374151" }}>– {c.desc}</span>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<div>
|
|
||||||
<button className="bb-btn bb-btn-md bb-btn-outline">
|
|
||||||
Узнать стоимость операции
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Правая часть — фото врача */}
|
|
||||||
<div
|
|
||||||
className="flex-1 relative overflow-hidden"
|
|
||||||
style={{ minHeight: 280 }}
|
|
||||||
>
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
<img
|
|
||||||
src="/hero-doctor.jpg"
|
|
||||||
alt="Врач на приёме с пациентом"
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
objectFit: "cover",
|
|
||||||
objectPosition: "center top",
|
|
||||||
position: "absolute",
|
|
||||||
inset: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Под баннером: соцсети */}
|
|
||||||
<div className="flex items-center gap-3 pt-1">
|
|
||||||
<span className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
Поделиться:
|
|
||||||
</span>
|
|
||||||
{["VK", "FB", "TW"].map((s) => (
|
|
||||||
<button
|
|
||||||
key={s}
|
|
||||||
className="text-xs px-2 py-1 rounded"
|
|
||||||
style={{
|
|
||||||
background: "var(--bb-sidebar-bg)",
|
|
||||||
border: "1px solid var(--bb-border)",
|
|
||||||
color: "var(--bb-text-muted)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{s}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
<span className="text-xs ml-2" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
👁 98 573 просмотра
|
|
||||||
</span>
|
|
||||||
</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: "#22c55e" }}>
|
|
||||||
✓
|
|
||||||
</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. Описание: обычный текст. Галочка: #22c55e.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* LLM блок */}
|
|
||||||
<LlmBlock path="/blocks/hero" version="v1.1" specText={LLM_HERO_TEXT}>
|
|
||||||
<LlmSection title="Структура баннера" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Зона", "Ширина", "Фон", "Содержимое"]}
|
|
||||||
rows={[
|
|
||||||
["Весь баннер", "100%", "#f9f4e7", "Единый светло-кремовый фон"],
|
|
||||||
["Левый блок", "~50%", "#f9f4e7 (общий)", "Заголовок uppercase + 3 галочки + кнопка outline"],
|
|
||||||
["Правый блок", "~50%", "#f9f4e7 (общий)", "Фото врача с пациентом"],
|
|
||||||
["Под баннером", "100%", "#fff", "Кнопки соцсетей + счётчик просмотров"],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Три пункта баннера" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Ключевое слово", "Описание"]}
|
|
||||||
rows={HERO_CHECKS.map((c) => [c.key, c.desc])}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Цвета" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Элемент", "Цвет", "Токен"]}
|
|
||||||
rows={[
|
|
||||||
["Фон баннера (единый)", "#f9f4e7", "Светло-кремовый фон"],
|
|
||||||
["Кнопка CTA", "outline-стиль", "bb-btn-outline"],
|
|
||||||
["Заголовок блока", "#111827", "—"],
|
|
||||||
["Галочка ✓", "#bf9975", "Бежевый (--brand-081m approx.)"],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Правила" />
|
|
||||||
<LlmRules
|
|
||||||
rules={[
|
|
||||||
{ ok: true, text: "Фон баннера: #f9f4e7 (светло-кремовый) — единый для всего блока" },
|
|
||||||
{ ok: true, text: "Кнопка CTA: bb-btn-outline (светлая)" },
|
|
||||||
{ 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import NavigationPageClient from "./NavigationPageClient";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Шапка и навигация. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function NavigationPage() {
|
||||||
|
return <NavigationPageClient />;
|
||||||
|
}
|
||||||
@@ -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,198 +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";
|
||||||
|
|
||||||
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.0
|
|
||||||
|
|
||||||
СТРУКТУРА БЛОКА:
|
|
||||||
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();
|
|
||||||
|
|
||||||
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>
|
|
||||||
<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>
|
|
||||||
<div
|
|
||||||
className="rounded-xl p-8 space-y-6"
|
|
||||||
style={{ background: "#fff", border: "1px solid var(--bb-border)" }}
|
|
||||||
>
|
|
||||||
<h2 className="text-xl font-bold" style={{ color: "#111827" }}>
|
|
||||||
Новости
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
||||||
{MOCK_NEWS.map((n) => (
|
|
||||||
<div
|
|
||||||
key={n.date}
|
|
||||||
className="bb-news-card rounded-lg p-3 cursor-pointer"
|
|
||||||
style={{
|
|
||||||
border: "1px solid var(--bb-border)",
|
|
||||||
transition: "background 0.15s, box-shadow 0.15s",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p className="text-xs mb-2" style={{ color: "#6b7280" }}>
|
|
||||||
{n.date}
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href={n.href}
|
|
||||||
className="text-sm font-medium leading-snug block"
|
|
||||||
style={{ color: "#0089c3", textDecoration: "none" }}
|
|
||||||
>
|
|
||||||
{n.title}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<button className="bb-btn bb-btn-md bb-btn-outline">
|
|
||||||
Все новости
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</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.0" 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,264 +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";
|
||||||
|
|
||||||
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.0
|
|
||||||
|
|
||||||
СТРУКТУРА БЛОКА:
|
|
||||||
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();
|
|
||||||
|
|
||||||
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>
|
|
||||||
<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>
|
|
||||||
<div
|
|
||||||
className="rounded-xl p-8 space-y-6"
|
|
||||||
style={{ background: "#fff", border: "1px solid var(--bb-border)" }}
|
|
||||||
>
|
|
||||||
{/* Заголовок */}
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-bold mb-2" style={{ color: "#111827" }}>
|
|
||||||
Отзывы о нас
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm" style={{ color: "#374151", lineHeight: 1.7 }}>
|
|
||||||
За 12 лет работы наши врачи оториноларингологи избавили от болезней ухо, горло, носа
|
|
||||||
более 50 000 пациентов. Но даже сейчас мы высоко ценим каждый положительный отзыв
|
|
||||||
и искренние слова благодарности.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Карусель (статичная) */}
|
|
||||||
<div className="relative flex items-center gap-4">
|
|
||||||
{/* Стрелка влево */}
|
|
||||||
<button
|
|
||||||
className="shrink-0 w-10 h-10 rounded-full flex items-center justify-center font-bold text-lg"
|
|
||||||
style={{
|
|
||||||
background: "var(--bb-sidebar-bg)",
|
|
||||||
border: "1px solid var(--bb-border)",
|
|
||||||
color: "var(--bb-text-muted)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
‹
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Карточка отзыва */}
|
|
||||||
<div
|
|
||||||
className="flex-1 rounded-xl p-6 relative"
|
|
||||||
style={{ background: "#eef4d1" }}
|
|
||||||
>
|
|
||||||
{/* Декоративная кавычка */}
|
|
||||||
<div
|
|
||||||
className="text-8xl leading-none mb-2 font-serif"
|
|
||||||
style={{ color: "#b8e6ed", lineHeight: 0.8 }}
|
|
||||||
>
|
|
||||||
«
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
className="text-sm italic leading-relaxed"
|
|
||||||
style={{ color: "#374151" }}
|
|
||||||
>
|
|
||||||
{MOCK_REVIEWS[0].text}
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="inline-block mt-3 text-sm"
|
|
||||||
style={{ color: "#0089c3" }}
|
|
||||||
>
|
|
||||||
Читать отзыв полностью
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Стрелка вправо */}
|
|
||||||
<button
|
|
||||||
className="shrink-0 w-10 h-10 rounded-full flex items-center justify-center font-bold text-lg"
|
|
||||||
style={{
|
|
||||||
background: "var(--bb-sidebar-bg)",
|
|
||||||
border: "1px solid var(--bb-border)",
|
|
||||||
color: "var(--bb-text-muted)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
›
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</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.0" specText={LLM_REVIEWS_TEXT}>
|
|
||||||
<LlmSection title="Структура блока" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Элемент", "Содержимое", "Стиль"]}
|
|
||||||
rows={[
|
|
||||||
["H2", "«Отзывы о нас»", "text-xl, font-bold, #111827"],
|
|
||||||
["Подзаголовок", "Описание достижений клиники за 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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/components/ui/Button";
|
||||||
import { CodeCopy } from "@/components/ui/CodeCopy";
|
import { CodeCopy } from "@/components/ui/CodeCopy";
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||||
|
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Кнопки. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
title: "Кнопки. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
@@ -227,6 +228,7 @@ a.callback_url {
|
|||||||
<h1 className="text-3xl font-semibold mb-3" style={{ color: "var(--bb-text)" }}>
|
<h1 className="text-3xl font-semibold mb-3" style={{ color: "var(--bb-text)" }}>
|
||||||
Кнопки
|
Кнопки
|
||||||
</h1>
|
</h1>
|
||||||
|
<BlockMetaBar path="/components/buttons" defaultVersion="v2.0" defaultIsInPreview={false} />
|
||||||
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}>
|
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
Кнопки скопированы с реального сайта{" "}
|
Кнопки скопированы с реального сайта{" "}
|
||||||
<span className="font-mono text-sm" style={{ color: "var(--bb-text)" }}>
|
<span className="font-mono text-sm" style={{ color: "var(--bb-text)" }}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { CodeCopy } from "@/components/ui/CodeCopy";
|
import { CodeCopy } from "@/components/ui/CodeCopy";
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||||
|
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Карточки. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
title: "Карточки. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
@@ -417,6 +418,7 @@ export default function CardsPage() {
|
|||||||
<h1 className="text-3xl font-semibold mb-3" style={{ color: "var(--bb-text)" }}>
|
<h1 className="text-3xl font-semibold mb-3" style={{ color: "var(--bb-text)" }}>
|
||||||
Карточки
|
Карточки
|
||||||
</h1>
|
</h1>
|
||||||
|
<BlockMetaBar path="/components/cards" defaultVersion="v1.0" defaultIsInPreview={false} />
|
||||||
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}>
|
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
Карточки врача, новости, отзыва, цены и услуги — основные блоки контента сайта.
|
Карточки врача, новости, отзыва, цены и услуги — основные блоки контента сайта.
|
||||||
Бейджи, теги и алерты — вспомогательные элементы.
|
Бейджи, теги и алерты — вспомогательные элементы.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import { Toggle } from "@/components/ui/Toggle";
|
import { Toggle } from "@/components/ui/Toggle";
|
||||||
import { CodeCopy } from "@/components/ui/CodeCopy";
|
import { CodeCopy } from "@/components/ui/CodeCopy";
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||||
|
import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Форм-контролы. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
title: "Форм-контролы. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
@@ -264,6 +265,7 @@ input[type=email] {
|
|||||||
<h1 className="text-3xl font-semibold mb-3" style={{ color: "var(--bb-text)" }}>
|
<h1 className="text-3xl font-semibold mb-3" style={{ color: "var(--bb-text)" }}>
|
||||||
Форм-контролы
|
Форм-контролы
|
||||||
</h1>
|
</h1>
|
||||||
|
<BlockMetaBar path="/components/forms" defaultVersion="v2.0" defaultIsInPreview={false} />
|
||||||
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}>
|
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
Элементы ввода данных: текстовые поля, выпадающие списки, флажки, переключатели.
|
Элементы ввода данных: текстовые поля, выпадающие списки, флажки, переключатели.
|
||||||
Применяются в формах записи, фильтрах и настройках.
|
Применяются в формах записи, фильтрах и настройках.
|
||||||
|
|||||||
@@ -1,300 +0,0 @@
|
|||||||
import type { Metadata } from "next";
|
|
||||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Шапка и навигация. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
|
||||||
};
|
|
||||||
|
|
||||||
const NAV_ITEMS = [
|
|
||||||
"Клиника",
|
|
||||||
"ЛОР врачи",
|
|
||||||
"Заболевания",
|
|
||||||
"Вопрос-ответ",
|
|
||||||
"ЛОР-операции",
|
|
||||||
"Сурдология",
|
|
||||||
"Цены",
|
|
||||||
"Контакты",
|
|
||||||
];
|
|
||||||
|
|
||||||
const LLM_NAV_TEXT = `
|
|
||||||
БЛОК: Шапка и навигация сайта (Header)
|
|
||||||
Источник: perm.oclinica.ru/lor — реальная шапка сайта
|
|
||||||
Версия: v1.0
|
|
||||||
|
|
||||||
СТРУКТУРА ШАПКИ (3 зоны сверху вниз):
|
|
||||||
|
|
||||||
1. ТОП-БАР (~40px высота, фон #fff, border-bottom 1px solid #e5e7eb)
|
|
||||||
Левая часть:
|
|
||||||
— Адрес: «Б. Цитная, 3» с иконкой 📍
|
|
||||||
— Ссылка: «Клиника ухо горло нос и аллергия»
|
|
||||||
— Ссылка: «Центр аллергологии и пульмонологии»
|
|
||||||
Правая часть:
|
|
||||||
— Телефон: «/342/ 255-53-84» (font-weight: bold, color: #111827)
|
|
||||||
— Кнопка «Заказать звонок» — стиль bb-btn-pill (#e9e4d4, border #d5cfbd, border-radius 25px)
|
|
||||||
|
|
||||||
2. ЛОГОТИП (~64px, фон #fff)
|
|
||||||
— Иконка кружок синий (#0089c3) с крестом
|
|
||||||
— Текст «КЛИНИКА УХО ГОРЛО НОС» жирный uppercase, цвет #53514e
|
|
||||||
— Подпись «им. проф. Е.Н.Оленевой» мелкий, цвет #9ca3af
|
|
||||||
|
|
||||||
3. ГЛАВНОЕ МЕНЮ (~46px, фон #fff, border-top 1px solid #e5e7eb)
|
|
||||||
Пункты (8 штук): Клиника | ЛОР врачи | Заболевания | Вопрос-ответ | ЛОР-операции | Сурдология | Цены | Контакты
|
|
||||||
Шрифт: Fira Sans 14px, weight 400
|
|
||||||
Цвет ссылок: #53514e (--brand-073m)
|
|
||||||
Активный / Hover: #0089c3 (--brand-053m)
|
|
||||||
Разделители: border-right 1px solid #f3f4f6 между пунктами
|
|
||||||
|
|
||||||
ПРАВИЛА:
|
|
||||||
✓ Логотип всегда кликабелен — ведёт на главную страницу раздела
|
|
||||||
✓ Кнопка «Заказать звонок» всегда видна, pill-стиль
|
|
||||||
✓ Телефон кликабелен (tel: ссылка)
|
|
||||||
✓ Активный пункт меню — цвет #0089c3, остальные #53514e
|
|
||||||
✕ Не добавлять пункты меню, которых нет на сайте
|
|
||||||
✕ Не менять порядок пунктов меню
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
export default function NavigationPage() {
|
|
||||||
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>
|
|
||||||
<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 overflow-hidden"
|
|
||||||
style={{ border: "1px solid var(--bb-border)", boxShadow: "0 1px 4px rgba(0,0,0,0.06)" }}
|
|
||||||
>
|
|
||||||
{/* Топ-бар */}
|
|
||||||
<div
|
|
||||||
className="flex items-center justify-between px-6 py-2 text-xs border-b"
|
|
||||||
style={{ background: "#fff", borderColor: "#e5e7eb", color: "#6b7280" }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<span>📍 Б. Цитная, 3</span>
|
|
||||||
<a href="#" style={{ color: "#0089c3" }}>
|
|
||||||
Клиника ухо горло нос и аллергия
|
|
||||||
</a>
|
|
||||||
<a href="#" style={{ color: "#0089c3" }}>
|
|
||||||
Центр аллергологии и пульмонологии
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<span className="font-bold text-sm" style={{ color: "#111827" }}>
|
|
||||||
/342/ 255-53-84
|
|
||||||
</span>
|
|
||||||
<button className="bb-btn bb-btn-sm bb-btn-pill">Заказать звонок</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Логотип */}
|
|
||||||
<div className="flex items-center px-6 py-3 bg-white">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div
|
|
||||||
className="w-10 h-10 rounded-full flex items-center justify-center text-white font-bold text-lg shrink-0"
|
|
||||||
style={{ background: "#0089c3" }}
|
|
||||||
>
|
|
||||||
✚
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className="text-[11px] font-bold leading-tight uppercase tracking-wide"
|
|
||||||
style={{ color: "#53514e" }}
|
|
||||||
>
|
|
||||||
Клиника<br />ухо, горло, нос
|
|
||||||
</div>
|
|
||||||
<div className="text-[9px] leading-tight mt-0.5" style={{ color: "#9ca3af" }}>
|
|
||||||
им. проф. Е.Н.Оленевой
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Навигация */}
|
|
||||||
<nav className="flex border-t bg-white" style={{ borderColor: "#e5e7eb" }}>
|
|
||||||
{NAV_ITEMS.map((item, i) => (
|
|
||||||
<a
|
|
||||||
key={item}
|
|
||||||
href="#"
|
|
||||||
className="px-4 py-3 text-sm bb-nav-link whitespace-nowrap"
|
|
||||||
style={{
|
|
||||||
color: i === 0 ? "#0089c3" : "#53514e",
|
|
||||||
borderRight: "1px solid #f3f4f6",
|
|
||||||
fontWeight: i === 0 ? 500 : 400,
|
|
||||||
textDecoration: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</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-3 gap-4">
|
|
||||||
{[
|
|
||||||
{
|
|
||||||
zone: "1. Топ-бар",
|
|
||||||
bg: "#f0f9ff",
|
|
||||||
height: "~40px",
|
|
||||||
color: "Фон: #fff",
|
|
||||||
desc: "Адрес, ссылки на разделы клиники, телефон, кнопка «Заказать звонок» (pill)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
zone: "2. Логотип",
|
|
||||||
bg: "#f0fdf4",
|
|
||||||
height: "~64px",
|
|
||||||
color: "Фон: #fff",
|
|
||||||
desc: "Иконка-кружок (#0089c3) + название клиники двумя строками + подпись мелким",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
zone: "3. Навигация",
|
|
||||||
bg: "#fefce8",
|
|
||||||
height: "~46px",
|
|
||||||
color: "Фон: #fff, border-top",
|
|
||||||
desc: "8 горизонтальных пунктов, активный = #0089c3, остальные = #53514e",
|
|
||||||
},
|
|
||||||
].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)" }}>
|
|
||||||
H: {z.height} · {z.color}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Пункты меню */}
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h2 className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
|
||||||
Пункты главного меню
|
|
||||||
</h2>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{NAV_ITEMS.map((item, i) => (
|
|
||||||
<span
|
|
||||||
key={item}
|
|
||||||
className="px-3 py-1.5 rounded text-sm"
|
|
||||||
style={{
|
|
||||||
background: i === 0 ? "#dff0fa" : "var(--bb-sidebar-bg)",
|
|
||||||
color: i === 0 ? "var(--brand-053m)" : "var(--bb-text)",
|
|
||||||
border: "1px solid var(--bb-border)",
|
|
||||||
fontWeight: i === 0 ? 500 : 400,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item}
|
|
||||||
{i === 0 && (
|
|
||||||
<span className="ml-1.5 text-[10px]" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
(активный)
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
|
||||||
Порядок пунктов фиксирован. Hover и активный пункт — цвет #0089c3 (--brand-053m).
|
|
||||||
</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: "#53514e", token: "--brand-073m" },
|
|
||||||
{ label: "Активный / hover", value: "#0089c3", token: "--brand-053m" },
|
|
||||||
{ label: "Кнопка «Заказать»", value: "#e9e4d4", token: "bb-btn-pill" },
|
|
||||||
].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="/components/navigation" version="v1.0" specText={LLM_NAV_TEXT}>
|
|
||||||
<LlmSection title="Зоны шапки" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Зона", "Высота", "Фон", "Содержимое"]}
|
|
||||||
rows={[
|
|
||||||
["Топ-бар", "~40px", "#fff", "Адрес, ссылки разделов, телефон /342/ 255-53-84, кнопка «Заказать звонок»"],
|
|
||||||
["Логотип", "~64px", "#fff", "Кружок #0089c3 + «КЛИНИКА УХО ГОРЛО НОС ИМ. ПРОФ. Е.Н.ОЛЕНЕВОЙ»"],
|
|
||||||
["Навигация", "~46px", "#fff + border-top", "8 пунктов: Клиника / ЛОР врачи / Заболевания / Вопрос-ответ / ЛОР-операции / Сурдология / Цены / Контакты"],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<LlmSection title="Цвета" />
|
|
||||||
<LlmTable
|
|
||||||
headers={["Элемент", "Цвет", "Токен"]}
|
|
||||||
rows={[
|
|
||||||
["Фон шапки", "#ffffff", "—"],
|
|
||||||
["Ссылки меню (default)", "#53514e", "--brand-073m"],
|
|
||||||
["Активный пункт / 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -421,9 +421,9 @@ export default function TypographyPage() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{[
|
{[
|
||||||
["H1 страницы (над Hero)", "Fira Sans", "~28–30px", "700", "#bf9975", "ЛОР Клиника ухо, горло, нос…"],
|
["H1 страницы (над Hero)", "Fira Sans", "36px", "700", "#cb9768", "ЛОР Клиника ухо, горло, нос…"],
|
||||||
["H2 секции", "Fira Sans", "~30px", "700", "#111827", "Приём ведут опытные ЛОР врачи"],
|
["H2 секции", "Fira Sans", "36px", "700", "#000000", "Приём ведут опытные ЛОР врачи"],
|
||||||
["Заголовок Hero-баннера", "Fira Sans", "~18px", "700", "#111827", "ЭНДОСКОПИЧЕСКОЕ ХИРУРГИЧЕСКОЕ…"],
|
["Заголовок Hero-баннера", "Fira Sans", "22px", "700", "#333333", "ЭНДОСКОПИЧЕСКОЕ ХИРУРГИЧЕСКОЕ…"],
|
||||||
["Статистика (цифра+текст)", "Fira Sans", "~18px", "700", "#60959c", "Ежедневно 27 ЛОР врачей…"],
|
["Статистика (цифра+текст)", "Fira Sans", "~18px", "700", "#60959c", "Ежедневно 27 ЛОР врачей…"],
|
||||||
["Подзаголовок секции", "Fira Sans", "~14px", "400", "#374151", "Фундаментальная теоретическая…"],
|
["Подзаголовок секции", "Fira Sans", "~14px", "400", "#374151", "Фундаментальная теоретическая…"],
|
||||||
["Пункт checklist (ключ)", "Fira Sans", "13px", "700", "#111827", "БЕЗОПАСНО"],
|
["Пункт checklist (ключ)", "Fira Sans", "13px", "700", "#111827", "БЕЗОПАСНО"],
|
||||||
|
|||||||
@@ -80,8 +80,8 @@ body {
|
|||||||
background: #FFA39C;
|
background: #FFA39C;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-color: #FF847B;
|
border-color: #FF847B;
|
||||||
border-radius: 7px;
|
border-radius: 4px;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
box-shadow: 0px 0px 5px rgba(0,0,0,0.4), 0px 3px 5px rgba(0,0,0,0.25);
|
box-shadow: 0px 0px 5px rgba(0,0,0,0.4), 0px 3px 5px rgba(0,0,0,0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +109,20 @@ body {
|
|||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Навигация сайта ────────────────────────────────────────── */
|
||||||
|
.bb-nav-item {
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.bb-nav-item:hover {
|
||||||
|
background: #f5f0e6;
|
||||||
|
}
|
||||||
|
.bb-nav-sub-item {
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
.bb-nav-sub-item:hover {
|
||||||
|
background: #f5f0e6;
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Форм-контролы (Sprint 3) ───────────────────────────────── */
|
/* ─── Форм-контролы (Sprint 3) ───────────────────────────────── */
|
||||||
.bb-input,
|
.bb-input,
|
||||||
.bb-textarea,
|
.bb-textarea,
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { NavigationBlock } from "@/components/blocks/NavigationBlock";
|
||||||
|
import { HeroBlock } from "@/components/blocks/HeroBlock";
|
||||||
|
import { DoctorsBlock } from "@/components/blocks/DoctorsBlock";
|
||||||
|
import { CeoBlock } from "@/components/blocks/CeoBlock";
|
||||||
|
import { ReviewsBlock } from "@/components/blocks/ReviewsBlock";
|
||||||
|
import { NewsBlock } from "@/components/blocks/NewsBlock";
|
||||||
|
import { ContactFormsBlock } from "@/components/blocks/ContactFormsBlock";
|
||||||
|
import { FooterBlock } from "@/components/blocks/FooterBlock";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "bb-preview-created";
|
||||||
|
const LS_PREFIX = "bb-block-preview:";
|
||||||
|
|
||||||
|
function BlockPlaceholder({ name, href }: { name: string; href: string }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full py-14 flex flex-col items-center justify-center rounded-xl gap-2"
|
||||||
|
style={{
|
||||||
|
border: "2px dashed #d1d5db",
|
||||||
|
background: "#f9fafb",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className="text-sm font-medium" style={{ color: "#6b7280" }}>
|
||||||
|
{name}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
className="text-xs underline"
|
||||||
|
style={{ color: "var(--brand-053m)" }}
|
||||||
|
>
|
||||||
|
Открыть в брендбуке →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlockDef {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
path: string;
|
||||||
|
defaultReady: boolean;
|
||||||
|
component?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BLOCKS: BlockDef[] = [
|
||||||
|
{
|
||||||
|
id: "navigation",
|
||||||
|
name: "Шапка / Навигация",
|
||||||
|
href: "/blocks/navigation",
|
||||||
|
path: "/blocks/navigation",
|
||||||
|
defaultReady: true,
|
||||||
|
component: <NavigationBlock />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "hero",
|
||||||
|
name: "Hero-баннер",
|
||||||
|
href: "/blocks/hero",
|
||||||
|
path: "/blocks/hero",
|
||||||
|
defaultReady: true,
|
||||||
|
component: <HeroBlock />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ceo",
|
||||||
|
name: "Вводный текст (CEO-блок)",
|
||||||
|
href: "/blocks/ceo",
|
||||||
|
path: "/blocks/ceo",
|
||||||
|
defaultReady: false,
|
||||||
|
component: <CeoBlock />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "doctors",
|
||||||
|
name: "Наши врачи",
|
||||||
|
href: "/blocks/doctors",
|
||||||
|
path: "/blocks/doctors",
|
||||||
|
defaultReady: true,
|
||||||
|
component: <DoctorsBlock />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "reviews",
|
||||||
|
name: "Отзывы",
|
||||||
|
href: "/blocks/reviews",
|
||||||
|
path: "/blocks/reviews",
|
||||||
|
defaultReady: false,
|
||||||
|
component: <ReviewsBlock />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "contact-forms",
|
||||||
|
name: "Формы записи",
|
||||||
|
href: "/blocks/contact-forms",
|
||||||
|
path: "/blocks/contact-forms",
|
||||||
|
defaultReady: false,
|
||||||
|
component: <ContactFormsBlock />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "news",
|
||||||
|
name: "Новости",
|
||||||
|
href: "/blocks/news",
|
||||||
|
path: "/blocks/news",
|
||||||
|
defaultReady: false,
|
||||||
|
component: <NewsBlock />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "footer",
|
||||||
|
name: "Подвал / Контакт",
|
||||||
|
href: "/blocks/contact",
|
||||||
|
path: "/blocks/contact",
|
||||||
|
defaultReady: false,
|
||||||
|
component: <FooterBlock />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function PreviewClient() {
|
||||||
|
const [created, setCreated] = useState(false);
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
// Map of path → isInPreview from API; null = not loaded yet
|
||||||
|
const [apiMeta, setApiMeta] = useState<Record<string, boolean> | null>(null);
|
||||||
|
|
||||||
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
|
|
||||||
|
// Counter to force re-render when localStorage preview toggles change
|
||||||
|
const [, setRefresh] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
if (localStorage.getItem(STORAGE_KEY) === "true") {
|
||||||
|
setCreated(true);
|
||||||
|
}
|
||||||
|
const handler = () => setRefresh((n) => n + 1);
|
||||||
|
window.addEventListener("bb-preview-change", handler);
|
||||||
|
return () => window.removeEventListener("bb-preview-change", handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!apiUrl) return;
|
||||||
|
fetch(`${apiUrl}/blocks`)
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((data: Array<{ path: string; isInPreview: boolean }>) => {
|
||||||
|
const map: Record<string, boolean> = {};
|
||||||
|
for (const b of data) map[b.path] = b.isInPreview;
|
||||||
|
setApiMeta(map);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// API offline — fall back to defaultReady
|
||||||
|
});
|
||||||
|
}, [apiUrl]);
|
||||||
|
|
||||||
|
function isReady(block: BlockDef): boolean {
|
||||||
|
// 1. API online → use API data
|
||||||
|
if (apiMeta !== null && block.path in apiMeta) {
|
||||||
|
return apiMeta[block.path] && !!block.component;
|
||||||
|
}
|
||||||
|
// 2. Check localStorage (set by BlockMetaBar toggle)
|
||||||
|
const lsVal = typeof window !== "undefined" ? localStorage.getItem(LS_PREFIX + block.path) : null;
|
||||||
|
if (lsVal !== null) {
|
||||||
|
return lsVal === "true" && !!block.component;
|
||||||
|
}
|
||||||
|
// 3. Fallback to defaultReady
|
||||||
|
return block.defaultReady && !!block.component;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreate() {
|
||||||
|
localStorage.setItem(STORAGE_KEY, "true");
|
||||||
|
setCreated(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRebuild() {
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
setCreated(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return null;
|
||||||
|
|
||||||
|
const readyCount = BLOCKS.filter(isReady).length;
|
||||||
|
|
||||||
|
/* ── ПУСТОЕ СОСТОЯНИЕ ── */
|
||||||
|
if (!created) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex flex-col items-center justify-center min-h-screen"
|
||||||
|
style={{ background: "var(--bb-bg)" }}
|
||||||
|
>
|
||||||
|
<div className="text-center max-w-md space-y-5 p-8">
|
||||||
|
<p
|
||||||
|
className="text-xs font-semibold uppercase tracking-widest"
|
||||||
|
style={{ color: "var(--brand-053m)" }}
|
||||||
|
>
|
||||||
|
Страницы
|
||||||
|
</p>
|
||||||
|
<h1 className="text-2xl font-bold" style={{ color: "var(--bb-text)" }}>
|
||||||
|
Просмотр текущей страницы
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm" style={{ color: "var(--bb-text-muted)", lineHeight: 1.7 }}>
|
||||||
|
Нажмите «Создать», чтобы собрать главную страницу{" "}
|
||||||
|
<span className="font-mono text-xs">perm.oclinica.ru/lor</span> из блоков,
|
||||||
|
задокументированных в брендбуке.
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center gap-2 text-xs px-4 py-2 rounded-lg"
|
||||||
|
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="inline-block w-2 h-2 rounded-full"
|
||||||
|
style={{ background: "#22c55e" }}
|
||||||
|
/>
|
||||||
|
<span style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
Готово блоков: {readyCount} из {BLOCKS.length}
|
||||||
|
</span>
|
||||||
|
<span style={{ color: "var(--bb-text-muted)" }}>·</span>
|
||||||
|
<span style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
{BLOCKS.length - readyCount} плейсхолдеров
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button onClick={handleCreate} className="bb-btn bb-btn-md bb-btn-primary">
|
||||||
|
Создать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── СОЗДАННОЕ СОСТОЯНИЕ ── */
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Топ-бар */}
|
||||||
|
<div
|
||||||
|
className="sticky top-0 z-10 flex items-center justify-between px-6 py-3 border-b"
|
||||||
|
style={{
|
||||||
|
background: "var(--bb-sidebar-bg)",
|
||||||
|
borderColor: "var(--bb-border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p
|
||||||
|
className="text-xs font-semibold uppercase tracking-widest"
|
||||||
|
style={{ color: "var(--brand-053m)" }}
|
||||||
|
>
|
||||||
|
Просмотр текущей страницы
|
||||||
|
</p>
|
||||||
|
<p className="text-xs mt-0.5" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
perm.oclinica.ru/lor · {readyCount}/{BLOCKS.length} блоков готово
|
||||||
|
{apiMeta === null && apiUrl && (
|
||||||
|
<span style={{ color: "#f59e0b" }}> · API загружается...</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={handleRebuild} className="bb-btn bb-btn-sm bb-btn-outline">
|
||||||
|
Пересобрать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Собранная страница */}
|
||||||
|
<div className="max-w-5xl mx-auto px-8 py-8 space-y-12">
|
||||||
|
{BLOCKS.map((block) =>
|
||||||
|
isReady(block) ? (
|
||||||
|
<section key={block.id}>{block.component}</section>
|
||||||
|
) : (
|
||||||
|
<section key={block.id}>
|
||||||
|
<BlockPlaceholder name={block.name} href={block.href} />
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { PreviewClient } from "./PreviewClient";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title:
|
||||||
|
"Просмотр текущей страницы. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PreviewPage() {
|
||||||
|
return <PreviewClient />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
export const CEO_QUESTIONS = [
|
||||||
|
"У вас болит ухо, заложен нос, першит в горле, и вы не можете понять причину?",
|
||||||
|
"Вам срочно нужен платный ЛОР в Перми или, как ещё говорят, «ухогорлонос»?",
|
||||||
|
"Заболел ребёнок?",
|
||||||
|
"Срочно ищете частные ЛОР-клиники Перми для детей 0+ и взрослых с удобным режимом работы с 9:00 до 21:00 по будням?",
|
||||||
|
"Вам назначили проведение эндоскопической операции на ухе, горле или носе?",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function CeoBlock() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "#fff",
|
||||||
|
fontFamily: "var(--font-web, 'Fira Sans', sans-serif)",
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 1.75,
|
||||||
|
color: "#374151",
|
||||||
|
padding: "40px 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Клиника ухо, нос специализируется на оториноларингологии – лечении взрослых и детей
|
||||||
|
с ЛОР заболеваниями. ЛОР клиника ухо, горло, нос в Перми представлена по двум адресам:{" "}
|
||||||
|
<a href="#" style={{ color: "#52b4bd" }}>ул. Клары Цеткин, 9</a> |{" "}
|
||||||
|
<a href="#" style={{ color: "#52b4bd" }}>ул. Г. Звезда, 31а</a>.{" "}
|
||||||
|
Это <a href="#" style={{ color: "#52b4bd" }}>Клиника лечения кашля и аллергии</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
{CEO_QUESTIONS.map((q) => (
|
||||||
|
<p key={q} style={{ marginBottom: 8 }}>— {q}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style={{ marginTop: 16 }}>
|
||||||
|
Обращайтесь в ЛОР центр ухо, горло, нос в Перми, наши врачи оториноларингологи обязательно Вам помогут!
|
||||||
|
</p>
|
||||||
|
<p style={{ marginTop: 8 }}>
|
||||||
|
Клиника ЛОР болезней ухо, горло, нос – это наиболее современный центр оториноларингологии в Перми,
|
||||||
|
благодаря эндоскопическому оборудованию, высокому профессионализму оториноларингологов.
|
||||||
|
</p>
|
||||||
|
<p style={{ marginTop: 16, fontWeight: 500 }}>Будьте здоровы!</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
export function ContactFormsBlock() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Форма 1: Будьте здоровы */}
|
||||||
|
<div
|
||||||
|
className="rounded-xl py-10 px-8 flex flex-col items-center gap-6"
|
||||||
|
style={{ background: "#d4f6f8" }}
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="font-bold mb-1" style={{ color: "#000000", fontSize: 36, lineHeight: "38px" }}>
|
||||||
|
Будьте здоровы!
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm" style={{ color: "#374151" }}>
|
||||||
|
Запишитесь на приём к врачу!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-full max-w-sm space-y-3">
|
||||||
|
<input
|
||||||
|
className="bb-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="Введите ваше имя"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="bb-input"
|
||||||
|
type="tel"
|
||||||
|
placeholder="Введите ваш телефон"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<select className="bb-select" disabled>
|
||||||
|
<option>Выберите ЛОР врача</option>
|
||||||
|
</select>
|
||||||
|
<label className="flex items-start gap-2 text-xs cursor-pointer" style={{ color: "#374151" }}>
|
||||||
|
<input type="checkbox" className="bb-checkbox mt-0.5" readOnly />
|
||||||
|
<span>
|
||||||
|
Отправляя данные, я даю согласие на обработку персональных данных
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<button className="bb-btn bb-btn-lg bb-btn-primary w-full">
|
||||||
|
Запишите меня!
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Форма 2: Узнайте стоимость */}
|
||||||
|
<div
|
||||||
|
className="rounded-xl py-10 px-8 flex flex-col items-center gap-6"
|
||||||
|
style={{ background: "#d4f6f8" }}
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="font-bold mb-1" style={{ color: "#000000", fontSize: 36, lineHeight: "38px" }}>
|
||||||
|
Узнайте стоимость операции
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm" style={{ color: "#374151" }}>
|
||||||
|
Проконсультируйтесь с ассистентом хирурга
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-full max-w-sm space-y-3">
|
||||||
|
<input
|
||||||
|
className="bb-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="Введите ваше имя"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="bb-input"
|
||||||
|
type="tel"
|
||||||
|
placeholder="Введите ваш телефон"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<label className="flex items-start gap-2 text-xs cursor-pointer" style={{ color: "#374151" }}>
|
||||||
|
<input type="checkbox" className="bb-checkbox mt-0.5" readOnly />
|
||||||
|
<span>
|
||||||
|
Отправляя данные, я даю согласие на обработку персональных данных
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<button className="bb-btn bb-btn-lg bb-btn-primary w-full">
|
||||||
|
Перезвоните мне
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
export const STATS = [
|
||||||
|
{ num: "27", label: "ЛОР врачей работает в клинике", prefix: "Ежедневно" },
|
||||||
|
{ num: "6", label: "кандидатов медицинских наук", prefix: "В том числе" },
|
||||||
|
{ num: "12 000+", label: "успешно проведённых операций", prefix: "Свыше" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DOCTORS = [
|
||||||
|
{
|
||||||
|
name: "Макарова Людмила Германовна",
|
||||||
|
spec: "ЛОР врач, сурдолог",
|
||||||
|
photo: "/doctors/makarova.jpg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Семерикова Наталия Александровна",
|
||||||
|
spec: "ЛОР врач, сурдолог, хирург. К.М.Н. Завед. Центром сурдологии",
|
||||||
|
photo: "/doctors/semerikova.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ворончихина Наталия Валерьевна",
|
||||||
|
spec: "Отоневролог, хирург. К.М.Н., доцент кафедры ПГМУ",
|
||||||
|
photo: "/doctors/voronchikhina.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Лобанова Ирина Юрьевна",
|
||||||
|
spec: "ЛОР врач, сурдолог",
|
||||||
|
photo: "/doctors/lobanova.jpg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Торсунова Наталья Сергеевна",
|
||||||
|
spec: "Специалист по слухопротезированию (сурдоакустик)",
|
||||||
|
photo: "/doctors/torsunova.jpg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Суворова Светлана Викторовна",
|
||||||
|
spec: "ЛОР врач, сурдолог",
|
||||||
|
photo: "/doctors/suvorova.jpg",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function DoctorsBlock() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Заголовок + описание */}
|
||||||
|
<div>
|
||||||
|
<h2 className="font-bold mb-3" style={{ color: "#000000", fontSize: 36, lineHeight: "38px" }}>
|
||||||
|
Приём ведут опытные ЛОР врачи
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm" style={{ color: "#374151", lineHeight: 1.7 }}>
|
||||||
|
Фундаментальная теоретическая подготовка и большой практический опыт в сочетании
|
||||||
|
с внимательным индивидуальным подходом являются причиной успеха лечения тысяч наших пациентов
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Статистика — без фона, border-bottom #60959c */}
|
||||||
|
<div className="grid grid-cols-3 gap-6">
|
||||||
|
{STATS.map((s) => (
|
||||||
|
<div key={s.num} className="pb-3" style={{ borderBottom: "3px solid #60959c" }}>
|
||||||
|
<p className="text-lg font-bold leading-snug" style={{ color: "#60959c" }}>
|
||||||
|
{s.prefix} {s.num} {s.label}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Сетка врачей — 6 колонок */}
|
||||||
|
<div className="grid grid-cols-6 gap-3">
|
||||||
|
{DOCTORS.map((doc) => (
|
||||||
|
<div key={doc.name} className="flex flex-col items-center text-center gap-1.5">
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src={doc.photo}
|
||||||
|
alt={doc.name}
|
||||||
|
style={{
|
||||||
|
width: 110,
|
||||||
|
height: 150,
|
||||||
|
objectFit: "cover",
|
||||||
|
objectPosition: "center top",
|
||||||
|
borderRadius: 4,
|
||||||
|
display: "block",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium leading-snug" style={{ color: "#60959c" }}>
|
||||||
|
{doc.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] mt-0.5 leading-snug" style={{ color: "#374151" }}>
|
||||||
|
{doc.spec}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
const FOOTER_COLUMNS = [
|
||||||
|
{
|
||||||
|
title: "О клинике",
|
||||||
|
links: [
|
||||||
|
"Врачи", "Цены", "Контакты", "Новости", "Отзывы", "История",
|
||||||
|
"Официальная информация", "Оборудование", "Слуховые аппараты",
|
||||||
|
"ЛОР конференции", "Вакансии", "Клиника кашля и аллергии", "Доверенность",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Заболевания",
|
||||||
|
links: [
|
||||||
|
"Заболевания уха", "Заболевания горла", "Заболевания носа",
|
||||||
|
"У детей", "У беременных",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Вопрос-ответ",
|
||||||
|
links: [
|
||||||
|
"Вопросы-ответы по заболеваниям уха",
|
||||||
|
"Вопросы-ответы по заболеваниям горла",
|
||||||
|
"Вопросы-ответы по заболеваниям носа",
|
||||||
|
"Вопросы-ответы по детским заболеваниям",
|
||||||
|
"Вопросы-ответы по операциям",
|
||||||
|
"Задать свой вопрос?",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Операции",
|
||||||
|
links: [
|
||||||
|
"Аденотомия", "Вазотомия", "Мастоидэктомия", "Мирингопластика",
|
||||||
|
"Оссикулопластика", "Септопластика", "Стапедопластика",
|
||||||
|
"Тимпанопластика", "Тонзиллотомия", "Микрогайморотомия", "Полипотомия",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function FooterBlock() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="rounded-xl overflow-hidden"
|
||||||
|
style={{ border: "1px solid var(--bb-border)" }}
|
||||||
|
>
|
||||||
|
{/* 4 колонки ссылок */}
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-4 gap-6 p-8"
|
||||||
|
style={{ background: "#f8f9fa", borderBottom: "1px solid #e5e7eb" }}
|
||||||
|
>
|
||||||
|
{FOOTER_COLUMNS.map((col) => (
|
||||||
|
<div key={col.title}>
|
||||||
|
<p className="font-semibold text-sm mb-3" style={{ color: "#111827" }}>
|
||||||
|
{col.title}
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-1.5">
|
||||||
|
{col.links.map((link) => (
|
||||||
|
<li key={link}>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="text-xs"
|
||||||
|
style={{ color: "#52b4bd", textDecoration: "none" }}
|
||||||
|
>
|
||||||
|
{link}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Нижняя полоса */}
|
||||||
|
<div
|
||||||
|
className="flex items-start justify-between gap-6 px-8 py-5"
|
||||||
|
style={{ background: "#fff" }}
|
||||||
|
>
|
||||||
|
{/* Логотип */}
|
||||||
|
<div className="flex items-center shrink-0">
|
||||||
|
<img
|
||||||
|
src="/logo/logo-main.png"
|
||||||
|
alt="Клиника ухо, горло, нос"
|
||||||
|
className="h-12 w-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Адреса и соцсети */}
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<div className="text-xs space-y-0.5" style={{ color: "#374151" }}>
|
||||||
|
<p>Мы находимся по адресам:</p>
|
||||||
|
<p>г. Пермь, ул. Клары Цеткин, 9</p>
|
||||||
|
<p>г. Пермь, ул. Газеты Звезда, 31А</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
{["VK", "YT", "TG", "OK", "Дзен"].map((s) => (
|
||||||
|
<a
|
||||||
|
key={s}
|
||||||
|
href="#"
|
||||||
|
className="text-[11px] px-2 py-1 rounded"
|
||||||
|
style={{
|
||||||
|
background: "var(--bb-sidebar-bg)",
|
||||||
|
border: "1px solid var(--bb-border)",
|
||||||
|
color: "var(--bb-text-muted)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Часы работы — два филиала */}
|
||||||
|
<div className="text-right text-xs space-y-2" style={{ color: "#374151" }}>
|
||||||
|
<p className="font-semibold text-xs" style={{ color: "#111827" }}>
|
||||||
|
Время работы клиники:
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium" style={{ color: "#111827" }}>Газеты Звезда 31а</p>
|
||||||
|
<p>пн – пт: с 09.00 до 21.00</p>
|
||||||
|
<p>сб – вс: с 09:00 до 19:00</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium" style={{ color: "#111827" }}>Клары Цеткин 9</p>
|
||||||
|
<p>пн – сб: с 09.00 до 17.00</p>
|
||||||
|
<p>вс – выходной</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
export const HERO_CHECKS = [
|
||||||
|
{ key: "БЕЗОПАСНО", desc: "оперируют хирурги с 15-летним опытом работы" },
|
||||||
|
{ key: "БЕЗ ВНЕШНИХ РАЗРЕЗОВ", desc: "хирургия сверхмалых размеров" },
|
||||||
|
{ key: "БЫСТРО", desc: "под наблюдением врача пациент находится 1 сутки" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function HeroBlock() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* H1 страницы */}
|
||||||
|
<h2
|
||||||
|
className="font-bold"
|
||||||
|
style={{ color: "#cb9768", fontSize: 36, lineHeight: "51px" }}
|
||||||
|
>
|
||||||
|
ЛОР Клиника ухо, горло, нос - медицинский центр лечения ЛОР заболеваний у детей и взрослых
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Баннер — единый светло-кремовый фон */}
|
||||||
|
<div
|
||||||
|
className="rounded-xl overflow-hidden flex"
|
||||||
|
style={{ background: "#f9f4e7", minHeight: 280 }}
|
||||||
|
>
|
||||||
|
{/* Левая часть — контент */}
|
||||||
|
<div
|
||||||
|
className="flex flex-col justify-center gap-5 p-8"
|
||||||
|
style={{ width: "50%", flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className="font-bold uppercase leading-snug"
|
||||||
|
style={{ color: "#333333", fontSize: 22 }}
|
||||||
|
>
|
||||||
|
Эндоскопическое хирургическое лечение ЛОР органов
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{HERO_CHECKS.map((c) => (
|
||||||
|
<li key={c.key} className="flex items-start gap-2 text-sm">
|
||||||
|
<span
|
||||||
|
className="shrink-0 font-bold"
|
||||||
|
style={{ color: "#bf9975", marginTop: 1 }}
|
||||||
|
>
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span className="font-bold uppercase" style={{ color: "#111827" }}>
|
||||||
|
{c.key}
|
||||||
|
</span>{" "}
|
||||||
|
<span style={{ color: "#374151" }}>– {c.desc}</span>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
<button className="bb-btn bb-btn-md bb-btn-pill">
|
||||||
|
Узнать стоимость операции
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Правая часть — фото врача */}
|
||||||
|
<div className="flex-1 relative overflow-hidden" style={{ minHeight: 280 }}>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src="/hero-doctor.jpg"
|
||||||
|
alt="Врач на приёме с пациентом"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
|
objectPosition: "center top",
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Под баннером: соцсети + просмотры */}
|
||||||
|
<div className="flex items-center gap-2 pt-1">
|
||||||
|
<button
|
||||||
|
className="text-xs px-3 py-1.5 rounded"
|
||||||
|
style={{
|
||||||
|
background: "#f9fafb",
|
||||||
|
border: "1px solid #e5e7eb",
|
||||||
|
color: "#9ca3af",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Поделиться ✉ 98572
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { NAV_ITEMS } from "./navData";
|
||||||
|
export { NAV_ITEMS };
|
||||||
|
|
||||||
|
function NavItem({ item, isActive }: { item: (typeof NAV_ITEMS)[number]; isActive: boolean }) {
|
||||||
|
const hasSubmenu = item.submenu.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div 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}
|
||||||
|
{hasSubmenu && (
|
||||||
|
<span className="text-xs" style={{ color: "#9ca3af", textDecoration: "none", display: "inline-block" }}>▾</span>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
{hasSubmenu && (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NavigationBlock() {
|
||||||
|
return (
|
||||||
|
<div className="bg-white relative">
|
||||||
|
{/* Верхняя панель: три столбца — логотип | ссылки | телефон+кнопка */}
|
||||||
|
<div className="flex items-center px-6 py-4">
|
||||||
|
{/* Столбец 1: Логотип */}
|
||||||
|
<div className="shrink-0">
|
||||||
|
<img
|
||||||
|
src="/logo/logo-main.png"
|
||||||
|
alt="Клиника ухо, горло, нос"
|
||||||
|
className="h-20 w-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Столбец 2: Три ссылки вертикально — прижат к логотипу */}
|
||||||
|
<div className="flex flex-col gap-1.5 ml-6">
|
||||||
|
<a href="#" className="flex items-center gap-1.5 text-sm" style={{ color: "#9ca3af", textDecoration: "none" }}>
|
||||||
|
<span style={{ color: "#9ca3af" }}>📍</span>
|
||||||
|
<span style={{ color: "#52b4bd", textDecoration: "underline" }}>К. Цеткин, 9</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" className="flex items-center gap-1.5 text-sm" style={{ color: "#9ca3af", textDecoration: "none" }}>
|
||||||
|
<span style={{ color: "#9ca3af" }}>📍</span>
|
||||||
|
<span style={{ color: "#52b4bd", textDecoration: "underline" }}>Клиника лечения кашля и аллергии</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" className="flex items-center gap-1.5 text-sm" style={{ color: "#9ca3af", textDecoration: "none" }}>
|
||||||
|
<span style={{ color: "#9ca3af" }}>📍</span>
|
||||||
|
<span style={{ color: "#52b4bd", textDecoration: "underline" }}>Центр диагностики и реабилитации</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Столбец 3: Телефон + кнопка вертикально — прижат вправо */}
|
||||||
|
<div className="flex flex-col items-end gap-2 shrink-0 ml-auto">
|
||||||
|
<span className="font-bold" style={{ color: "#000", fontSize: 25 }}>
|
||||||
|
/342/ 255-53-84
|
||||||
|
</span>
|
||||||
|
<button className="bb-btn bb-btn-md bb-btn-pill">Заказать звонок</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Главное меню */}
|
||||||
|
<nav className="flex bg-white">
|
||||||
|
{NAV_ITEMS.map((item, i) => (
|
||||||
|
<NavItem key={item.label} item={item} isActive={i === 0} />
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
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: "#",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function NewsBlock() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="rounded-xl p-8 space-y-6"
|
||||||
|
style={{ background: "#f2fee6" }}
|
||||||
|
>
|
||||||
|
<h2 className="font-bold" style={{ color: "#000000", fontSize: 36, lineHeight: "38px" }}>
|
||||||
|
Новости
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
{MOCK_NEWS.map((n) => (
|
||||||
|
<div
|
||||||
|
key={n.date}
|
||||||
|
className="bb-news-card rounded-lg p-3 cursor-pointer"
|
||||||
|
style={{
|
||||||
|
border: "1px solid var(--bb-border)",
|
||||||
|
transition: "background 0.15s, box-shadow 0.15s",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className="text-xs mb-2" style={{ color: "#6b7280" }}>
|
||||||
|
{n.date}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={n.href}
|
||||||
|
className="text-sm font-medium leading-snug block"
|
||||||
|
style={{ color: "#0089c3", textDecoration: "none" }}
|
||||||
|
>
|
||||||
|
{n.title}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<button className="bb-btn bb-btn-md bb-btn-outline">
|
||||||
|
Все новости
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
const MOCK_REVIEWS = [
|
||||||
|
{
|
||||||
|
text: "Спасибо за приём, мне всё понравилось, спасибо за приём, мне всё понравилось. Врач очень внимательный и профессиональный.",
|
||||||
|
author: "Пациент клиники",
|
||||||
|
doctor: "Тимофеева Наталья Александровна",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Очень довольна лечением! Прошла курс процедур, нос дышит отлично. Рекомендую клинику всем.",
|
||||||
|
author: "Наталья К.",
|
||||||
|
doctor: "Макарова Людмила Тимофеевна",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ReviewsBlock() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="rounded-xl p-8 space-y-6"
|
||||||
|
style={{ background: "#fff", border: "1px solid var(--bb-border)" }}
|
||||||
|
>
|
||||||
|
{/* Заголовок */}
|
||||||
|
<div>
|
||||||
|
<h2 className="font-bold mb-2" style={{ color: "#000000", fontSize: 36, lineHeight: "38px" }}>
|
||||||
|
Отзывы о нас
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm" style={{ color: "#374151", lineHeight: 1.7 }}>
|
||||||
|
За 12 лет работы наши врачи оториноларингологи избавили от болезней ухо, горло, носа
|
||||||
|
более 50 000 пациентов. Но даже сейчас мы высоко ценим каждый положительный отзыв
|
||||||
|
и искренние слова благодарности.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Карусель */}
|
||||||
|
<div className="relative flex items-center gap-4">
|
||||||
|
{/* Стрелка влево */}
|
||||||
|
<button
|
||||||
|
className="shrink-0 w-10 h-10 rounded-full flex items-center justify-center font-bold text-lg"
|
||||||
|
style={{
|
||||||
|
background: "var(--bb-sidebar-bg)",
|
||||||
|
border: "1px solid var(--bb-border)",
|
||||||
|
color: "var(--bb-text-muted)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Карточка отзыва */}
|
||||||
|
<div
|
||||||
|
className="flex-1 rounded-xl p-6"
|
||||||
|
style={{ background: "#eef4d1" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="text-8xl leading-none mb-2 font-serif"
|
||||||
|
style={{ color: "#b8e6ed", lineHeight: 0.8 }}
|
||||||
|
>
|
||||||
|
«
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
className="text-sm italic leading-relaxed"
|
||||||
|
style={{ color: "#374151" }}
|
||||||
|
>
|
||||||
|
{MOCK_REVIEWS[0].text}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="inline-block mt-3 text-sm"
|
||||||
|
style={{ color: "#0089c3" }}
|
||||||
|
>
|
||||||
|
Читать отзыв полностью
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Стрелка вправо */}
|
||||||
|
<button
|
||||||
|
className="shrink-0 w-10 h-10 rounded-full flex items-center justify-center font-bold text-lg"
|
||||||
|
style={{
|
||||||
|
background: "var(--bb-sidebar-bg)",
|
||||||
|
border: "1px solid var(--bb-border)",
|
||||||
|
color: "var(--bb-text-muted)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
export const NAV_ITEMS = [
|
||||||
|
{
|
||||||
|
label: "Клиника",
|
||||||
|
submenu: [
|
||||||
|
"О клинике", "Официальная информация", "История", "Оборудование",
|
||||||
|
"Новости", "Отзывы", "ЛОР конференции", "Вакансии", "Доверенность",
|
||||||
|
"Клиника лечения кашля и аллергии", "Налоговый вычет",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ label: "ЛОР врачи", submenu: [] as string[] },
|
||||||
|
{
|
||||||
|
label: "Заболевания",
|
||||||
|
submenu: ["Горло", "Ухо", "Нос", "У детей", "У беременных", "Диагностика"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Вопрос-ответ",
|
||||||
|
submenu: [
|
||||||
|
"Вопросы по заболеваниям уха", "Вопросы по заболеваниям горла",
|
||||||
|
"Вопросы по заболеваниям носа", "Вопросы по детским заболеваниям",
|
||||||
|
"Вопросы по операциям", "Задать свой вопрос?",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "ЛОР операции",
|
||||||
|
submenu: [
|
||||||
|
"Аденотомия", "Вазотомия", "Мастоидэктомия", "Тимпанопластика 1 типа",
|
||||||
|
"Тимпанопластика 2 типа", "Оссикулопластика", "Септопластика",
|
||||||
|
"Стапедопластика", "Тонзиллотомия", "Полипотомия", "Микрогайморотомия",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ label: "Сурдология", submenu: [] as string[] },
|
||||||
|
{ label: "Цены", submenu: [] as string[] },
|
||||||
|
{ label: "Контакты", submenu: [] as string[] },
|
||||||
|
];
|
||||||
@@ -30,12 +30,12 @@ const NAV: NavSection[] = [
|
|||||||
{ label: "Кнопки", href: "/components/buttons" },
|
{ label: "Кнопки", href: "/components/buttons" },
|
||||||
{ label: "Форм-контролы", href: "/components/forms" },
|
{ label: "Форм-контролы", href: "/components/forms" },
|
||||||
{ label: "Карточки", href: "/components/cards" },
|
{ label: "Карточки", href: "/components/cards" },
|
||||||
{ label: "Шапка / Навигация", href: "/components/navigation" },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Блоки",
|
title: "Блоки",
|
||||||
items: [
|
items: [
|
||||||
|
{ label: "Шапка / Навигация", href: "/blocks/navigation" },
|
||||||
{ label: "Hero-баннер", href: "/blocks/hero" },
|
{ label: "Hero-баннер", href: "/blocks/hero" },
|
||||||
{ label: "Вводный текст", href: "/blocks/ceo" },
|
{ label: "Вводный текст", href: "/blocks/ceo" },
|
||||||
{ label: "Наши врачи", href: "/blocks/doctors" },
|
{ label: "Наши врачи", href: "/blocks/doctors" },
|
||||||
@@ -48,6 +48,7 @@ const NAV: NavSection[] = [
|
|||||||
{
|
{
|
||||||
title: "Страницы",
|
title: "Страницы",
|
||||||
items: [
|
items: [
|
||||||
|
{ label: "Просмотр страницы", href: "/pages/preview" },
|
||||||
{ label: "Главная", href: "/pages/home", soon: true },
|
{ label: "Главная", href: "/pages/home", soon: true },
|
||||||
{ label: "Заболевание", href: "/pages/disease", soon: true },
|
{ label: "Заболевание", href: "/pages/disease", soon: true },
|
||||||
{ label: "Все врачи", href: "/pages/doctors", soon: true },
|
{ label: "Все врачи", href: "/pages/doctors", soon: true },
|
||||||
@@ -166,7 +167,7 @@ export function Sidebar() {
|
|||||||
color: "var(--bb-sidebar-text-muted)",
|
color: "var(--bb-sidebar-text-muted)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Sprint 5 · v0.5.1
|
Sprint 6 · v0.6.0
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,364 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { BlockChangelog, type ChangelogEntry } from "./BlockChangelog";
|
||||||
|
|
||||||
|
const LS_PREFIX = "bb-block-preview:";
|
||||||
|
|
||||||
|
function readLocalPreview(path: string, fallback: boolean): boolean {
|
||||||
|
if (typeof window === "undefined") return fallback;
|
||||||
|
const v = localStorage.getItem(LS_PREFIX + path);
|
||||||
|
if (v === null) return fallback;
|
||||||
|
return v === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeLocalPreview(path: string, value: boolean) {
|
||||||
|
localStorage.setItem(LS_PREFIX + path, String(value));
|
||||||
|
window.dispatchEvent(new Event("bb-preview-change"));
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlockMeta {
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
isInPreview: boolean;
|
||||||
|
changelog: ChangelogEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SnapshotListItem {
|
||||||
|
id: string;
|
||||||
|
version: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SnapshotData {
|
||||||
|
html: string;
|
||||||
|
css: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlockMetaBarProps {
|
||||||
|
path: string;
|
||||||
|
defaultVersion: string;
|
||||||
|
defaultIsInPreview: boolean;
|
||||||
|
defaultChangelog?: ChangelogEntry[];
|
||||||
|
onSnapshotSelect?: (snapshot: SnapshotData | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 [editing, setEditing] = useState(false);
|
||||||
|
const [versionInput, setVersionInput] = useState(defaultVersion);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [savingVersion, setSavingVersion] = useState<false | 'saving' | 'done'>(false);
|
||||||
|
const [togglingPreview, setTogglingPreview] = useState(false);
|
||||||
|
const [apiDown, setApiDown] = useState(false);
|
||||||
|
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;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalPreview(readLocalPreview(path, defaultIsInPreview));
|
||||||
|
}, [path, defaultIsInPreview]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!apiUrl) { setApiDown(true); return; }
|
||||||
|
fetch(`${apiUrl}/blocks/by-path?path=${encodeURIComponent(path)}`)
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((data: BlockMeta) => {
|
||||||
|
setMeta(data);
|
||||||
|
setVersionInput(data.version);
|
||||||
|
})
|
||||||
|
.catch(() => setApiDown(true));
|
||||||
|
}, [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[] }) {
|
||||||
|
if (!apiUrl) return null;
|
||||||
|
const r = await fetch(`${apiUrl}/blocks/by-path?path=${encodeURIComponent(path)}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
return r.json() as Promise<BlockMeta>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveVersion() {
|
||||||
|
if (!meta) return;
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const updated = await patch({ version: versionInput });
|
||||||
|
if (updated) { setMeta(updated); setEditing(false); }
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveFullVersion() {
|
||||||
|
if (apiDown || !apiUrl) return;
|
||||||
|
setSavingVersion('saving');
|
||||||
|
try {
|
||||||
|
// 1. Update block metadata (prefer current API version over hardcoded default)
|
||||||
|
const currentVersion = meta?.version ?? defaultVersion;
|
||||||
|
const updated = await patch({ version: currentVersion, changelog: defaultChangelog });
|
||||||
|
if (updated) {
|
||||||
|
setMeta(updated);
|
||||||
|
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: currentVersion, changelog: defaultChangelog, html, css }),
|
||||||
|
});
|
||||||
|
await fetchSnapshots();
|
||||||
|
} catch { /* snapshot capture may fail if container not found */ }
|
||||||
|
|
||||||
|
setSavingVersion('done');
|
||||||
|
setTimeout(() => setSavingVersion(false), 1500);
|
||||||
|
} catch {
|
||||||
|
setSavingVersion(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function togglePreview() {
|
||||||
|
if (apiDown) {
|
||||||
|
const newVal = !localPreview;
|
||||||
|
setLocalPreview(newVal);
|
||||||
|
writeLocalPreview(path, newVal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!meta) return;
|
||||||
|
setTogglingPreview(true);
|
||||||
|
try {
|
||||||
|
const updated = await patch({ isInPreview: !meta.isInPreview });
|
||||||
|
if (updated) setMeta(updated);
|
||||||
|
} finally {
|
||||||
|
setTogglingPreview(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 isInPreview = apiDown ? localPreview : (meta?.isInPreview ?? localPreview);
|
||||||
|
const changelog: ChangelogEntry[] = (meta?.changelog && meta.changelog.length > 0) ? meta.changelog : defaultChangelog;
|
||||||
|
const isViewingSnapshot = selectedSnapshotId !== "current";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
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 selector */}
|
||||||
|
<span className="font-semibold" style={{ color: "var(--bb-text-muted)" }}>
|
||||||
|
Версия:
|
||||||
|
</span>
|
||||||
|
{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">
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loadingSnapshot && (
|
||||||
|
<span className="text-xs" style={{ color: "var(--bb-text-muted)" }}>загрузка...</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span style={{ color: "var(--bb-border)" }}>·</span>
|
||||||
|
|
||||||
|
{/* Preview toggle */}
|
||||||
|
<button
|
||||||
|
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>
|
||||||
|
|
||||||
|
<span style={{ color: "var(--bb-border)" }}>·</span>
|
||||||
|
|
||||||
|
{/* Save version button */}
|
||||||
|
<button
|
||||||
|
onClick={saveFullVersion}
|
||||||
|
disabled={apiDown || savingVersion === 'saving' || isViewingSnapshot}
|
||||||
|
title={apiDown ? 'API недоступен' : isViewingSnapshot ? 'Переключитесь на текущую версию' : 'Сохранить текущую версию и changelog в БД'}
|
||||||
|
className="px-2.5 py-1 rounded font-medium transition-colors"
|
||||||
|
style={savingVersion === 'done' ? {
|
||||||
|
background: "#dcfce7",
|
||||||
|
color: "#16a34a",
|
||||||
|
border: "1px solid #86efac",
|
||||||
|
cursor: "default",
|
||||||
|
} : {
|
||||||
|
background: "var(--bb-sidebar-bg)",
|
||||||
|
color: (apiDown || isViewingSnapshot) ? "var(--bb-text-muted)" : "var(--brand-053m)",
|
||||||
|
border: "1px solid var(--bb-border)",
|
||||||
|
cursor: (apiDown || isViewingSnapshot) ? "default" : "pointer",
|
||||||
|
opacity: (apiDown || isViewingSnapshot) ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{savingVersion === 'saving' ? '...' : savingVersion === 'done' ? 'Сохранено' : 'Сохранить версию'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Subtle offline dot */}
|
||||||
|
{apiDown && (
|
||||||
|
<span
|
||||||
|
className="inline-block w-1.5 h-1.5 rounded-full"
|
||||||
|
title="API недоступен — данные из кода"
|
||||||
|
style={{ background: "#d1d5db" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</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.length > 0 && <BlockChangelog changelog={changelog} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-1
@@ -8,7 +8,7 @@ services:
|
|||||||
POSTGRES_PASSWORD: brandbook
|
POSTGRES_PASSWORD: brandbook
|
||||||
POSTGRES_DB: brandbook
|
POSTGRES_DB: brandbook
|
||||||
ports:
|
ports:
|
||||||
- "5433:5432"
|
- "5434:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
|||||||
+62
-11
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
## Клиника ухо, горло, нос им. проф. Е.Н.Оленевой
|
## Клиника ухо, горло, нос им. проф. Е.Н.Оленевой
|
||||||
|
|
||||||
**Версия контекста:** 4.2
|
**Версия контекста:** 4.5
|
||||||
**Дата обновления:** 2026-03-23
|
**Дата обновления:** 2026-03-25
|
||||||
**Актуальный спринт:** Sprint 5
|
**Актуальный спринт:** Sprint 6
|
||||||
**Сайт клиники:** https://oclinica.ru
|
**Сайт клиники:** https://oclinica.ru
|
||||||
**Брендбук (локально):** http://localhost:3001
|
**Брендбук (локально):** http://localhost:3001
|
||||||
**Брендбук (production):** https://web-oclinica.vercel.app
|
**Брендбук (production):** https://web-oclinica.vercel.app
|
||||||
@@ -67,8 +67,9 @@
|
|||||||
| Коралловый | `#ffa39c` | rgb(255,163,156) | Акценты | 2 | CTA-кнопки («Запишите меня!») |
|
| Коралловый | `#ffa39c` | rgb(255,163,156) | Акценты | 2 | CTA-кнопки («Запишите меня!») |
|
||||||
| Основной текст | `#464646` | rgb(70,70,70) | Текст | 3 | Цвет основного текста на сайте |
|
| Основной текст | `#464646` | rgb(70,70,70) | Текст | 3 | Цвет основного текста на сайте |
|
||||||
| Второстепенный текст | `#949290` | rgb(148,146,144) | Текст | 4 | Подписи, второстепенный контент |
|
| Второстепенный текст | `#949290` | rgb(148,146,144) | Текст | 4 | Подписи, второстепенный контент |
|
||||||
|
| H1 страницы | `#cb9768` | rgb(203,151,104) | Текст | 1 | Цвет H1 на страницах разделов |
|
||||||
| Светло-кремовый Hero | `#f9f4e7` | rgb(249,244,231) | Фоны | 1 | Фон Hero-баннера страниц разделов |
|
| Светло-кремовый Hero | `#f9f4e7` | rgb(249,244,231) | Фоны | 1 | Фон Hero-баннера страниц разделов |
|
||||||
| Светло-бирюзовый фон | `#b8e6ed` | rgb(184,230,237) | Фоны | 1 | Фон формы записи («Будьте здоровы!») |
|
| Светло-бирюзовый фон | `#d4f6f8` | rgb(212,246,248) | Фоны | 2 | Фон обеих форм записи (ранее #b8e6ed) |
|
||||||
| Кремовый фон | `#e9e4d4` | rgb(233,228,212) | Фоны | 1 | Тёплые фоны кнопок-пилюль, вторичные секции |
|
| Кремовый фон | `#e9e4d4` | rgb(233,228,212) | Фоны | 1 | Тёплые фоны кнопок-пилюль, вторичные секции |
|
||||||
| Светло-жёлтый фон | `#eef4d1` | rgb(238,244,209) | Фоны | 1 | Фон карточек отзывов |
|
| Светло-жёлтый фон | `#eef4d1` | rgb(238,244,209) | Фоны | 1 | Фон карточек отзывов |
|
||||||
| Светло-зелёный фон | `#f2fee6` | rgb(242,254,230) | Фоны | 1 | Фон секции новостей |
|
| Светло-зелёный фон | `#f2fee6` | rgb(242,254,230) | Фоны | 1 | Фон секции новостей |
|
||||||
@@ -290,10 +291,16 @@ SVG-версия ожидается (не получена от клиники).
|
|||||||
| `/components/forms` | ✅ Готова | Форм-контролы — Input/Textarea/Select/Checkbox/Radio/Toggle |
|
| `/components/forms` | ✅ Готова | Форм-контролы — Input/Textarea/Select/Checkbox/Radio/Toggle |
|
||||||
| `/components/cards` | ✅ Sprint 4 | Карточки — врач, услуга, новость, отзыв, цена + бейджи/теги/алерты |
|
| `/components/cards` | ✅ Sprint 4 | Карточки — врач, услуга, новость, отзыв, цена + бейджи/теги/алерты |
|
||||||
| `/components/*` | 🔜 Sprint 4–5 | Модалки, таблицы, навигация |
|
| `/components/*` | 🔜 Sprint 4–5 | Модалки, таблицы, навигация |
|
||||||
| `/blocks/hero` | ✅ Sprint 5 v1.1 | Hero-баннер: фон #f9f4e7, реальное фото врача, галочки #bf9975, кнопка outline |
|
| `/blocks/navigation` | ✅ Sprint 5 v1.3 | Шапка: топ-бар, логотип, главное меню с подменю |
|
||||||
| `/blocks/doctors` | ✅ Sprint 5 v1.1 | Врачи: H2 30px, статистика #60959c без фона, 6 реальных фото |
|
| `/blocks/hero` | ✅ Sprint 5 v1.3 | Hero-баннер: H1 36px #cb9768, заголовок 22px #333, кнопка pill, фон #f9f4e7 |
|
||||||
| `/blocks/*` | 🔜 Sprint 5 | Отзывы, формы, новости, footer |
|
| `/blocks/ceo` | ✅ Sprint 5 v1.2 | Вводный текст: специализация клиники, вопросы-стимулы |
|
||||||
| `/pages/*` | 🔜 Sprint 9–11 | Главная, заболевание, врачи, цены, контакты |
|
| `/blocks/doctors` | ✅ Sprint 5 v1.2 | Врачи: H2 36px #000, статистика #60959c без фона, 6 реальных фото |
|
||||||
|
| `/blocks/reviews` | ✅ Sprint 5 v1.1 | Отзывы: карусель, кавычка, стрелки навигации |
|
||||||
|
| `/blocks/contact-forms` | ✅ Sprint 5 v1.2 | Формы записи: «Будьте здоровы!» + «Узнайте стоимость» |
|
||||||
|
| `/blocks/news` | ✅ Sprint 5 v1.1 | Новости: 4 карточки, фон #f2fee6 |
|
||||||
|
| `/blocks/contact` | ✅ Sprint 5 v1.1 | Footer: 4 колонки ссылок, адреса, соцсети |
|
||||||
|
| `/pages/preview` | ✅ Sprint 5.5 | Просмотр страницы: сборка блоков с toggle «В превью» |
|
||||||
|
| `/pages/*` | 🔜 Sprint 6 | Главная, заболевание, врачи, цены, контакты |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -415,7 +422,47 @@ CSS-классы в `globals.css`. Компонент: `@/components/ui/Button`
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Технический стек проекта
|
## 10. Система версий блоков
|
||||||
|
|
||||||
|
Каждый блок поддерживает версионирование с HTML-снимками.
|
||||||
|
|
||||||
|
### Модель данных
|
||||||
|
|
||||||
|
**Block** — текущее состояние блока:
|
||||||
|
- `path` (unique), `version`, `isInPreview`, `changelog` (JSON), `name`
|
||||||
|
|
||||||
|
**BlockSnapshot** — архивный снимок версии:
|
||||||
|
- `blockPath`, `version` (unique пара), `html` (Text), `css` (Text), `changelog` (JSON)
|
||||||
|
|
||||||
|
### API эндпоинты
|
||||||
|
|
||||||
|
| Метод | URL | Назначение |
|
||||||
|
|-------|-----|------------|
|
||||||
|
| GET | `/blocks` | Все блоки |
|
||||||
|
| GET | `/blocks/by-path?path={path}` | Блок по пути (auto-create) |
|
||||||
|
| PATCH | `/blocks/by-path?path={path}` | Обновить version/isInPreview/changelog |
|
||||||
|
| POST | `/blocks/snapshots?path={path}` | Создать/обновить HTML-снимок версии |
|
||||||
|
| GET | `/blocks/snapshots?path={path}` | Список снимков (id, version, createdAt) |
|
||||||
|
| GET | `/blocks/snapshots/:id` | Полный снимок (html + css) |
|
||||||
|
|
||||||
|
### Компоненты
|
||||||
|
|
||||||
|
- **BlockMetaBar** (`components/ui/BlockMetaBar.tsx`) — версия dropdown, toggle «В превью», кнопка «Сохранить версию»
|
||||||
|
- Prop `onSnapshotSelect` — callback при выборе архивной версии
|
||||||
|
- При сохранении захватывает `innerHTML` из `[data-block-capture="{path}"]` + CSS правила с `bb-` префиксом
|
||||||
|
- **BlockChangelog** (`components/ui/BlockChangelog.tsx`) — рендер истории версий
|
||||||
|
|
||||||
|
### Архитектура страниц блоков
|
||||||
|
|
||||||
|
Каждая страница блока разделена на:
|
||||||
|
- `page.tsx` — Server Component (экспортирует `metadata`)
|
||||||
|
- `XxxPageClient.tsx` — Client Component (state для snapshot, `data-block-capture` для захвата)
|
||||||
|
|
||||||
|
При просмотре архивной версии живой компонент заменяется на `dangerouslySetInnerHTML` со снимком.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Технический стек проекта
|
||||||
|
|
||||||
| Слой | Технология | Версия |
|
| Слой | Технология | Версия |
|
||||||
|------|-----------|--------|
|
|------|-----------|--------|
|
||||||
@@ -430,7 +477,7 @@ CSS-классы в `globals.css`. Компонент: `@/components/ui/Button`
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. История изменений контекста
|
## 12. История изменений контекста
|
||||||
|
|
||||||
| Версия | Дата | Что добавлено |
|
| Версия | Дата | Что добавлено |
|
||||||
|--------|------|---------------|
|
|--------|------|---------------|
|
||||||
@@ -440,10 +487,14 @@ CSS-классы в `globals.css`. Компонент: `@/components/ui/Button`
|
|||||||
| 3.0 | 2026-03-22 | Sprint 3: кнопки (Button), форм-контролы (Input/Textarea/Select/Checkbox/Radio/Toggle), LLM-блок на логотипе |
|
| 3.0 | 2026-03-22 | Sprint 3: кнопки (Button), форм-контролы (Input/Textarea/Select/Checkbox/Radio/Toggle), LLM-блок на логотипе |
|
||||||
| 4.0 | 2026-03-22 | Sprint 4 start: исправлены цвета Oracal (точные RGB из каталога), кнопки/формы по реальному сайту |
|
| 4.0 | 2026-03-22 | Sprint 4 start: исправлены цвета Oracal (точные RGB из каталога), кнопки/формы по реальному сайту |
|
||||||
| 4.1 | 2026-03-22 | Sprint 4 done: карточки (DoctorCard/NewsCard/ReviewCard/PriceCard/ServiceCard), бейджи/теги/алерты |
|
| 4.1 | 2026-03-22 | Sprint 4 done: карточки (DoctorCard/NewsCard/ReviewCard/PriceCard/ServiceCard), бейджи/теги/алерты |
|
||||||
|
| 4.2 | 2026-03-23 | Sprint 5: блоки Hero v1.1, Doctors v1.1 |
|
||||||
|
| 4.3 | 2026-03-24 | Sprint 5.5: исправлены CSS-стили блоков по реальному сайту (H1 #cb9768 36px, H2 #000 36px, фоны форм #d4f6f8, фон новостей #f2fee6, CTA-кнопка pill) |
|
||||||
|
| 4.4 | 2026-03-25 | Sprint 5.5: кнопка «Сохранить версию» (snapshot HTML+CSS в PostgreSQL), dropdown переключения между версиями блоков, навигация v1.3 с подменю, блоки обновлены из предыдущей сессии |
|
||||||
|
| 4.5 | 2026-03-25 | Sprint 5/5.5 завершены: все 8 блоков /lor задокументированы, CEO v1.2, исправлен баг saveFullVersion() (откат версии), таблица блоков обновлена |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. Что обновлять в этом файле
|
## 13. Что обновлять в этом файле
|
||||||
|
|
||||||
При каждом спринте добавляй:
|
При каждом спринте добавляй:
|
||||||
- Новые компоненты и их спецификации (цвета, размеры, состояния)
|
- Новые компоненты и их спецификации (цвета, размеры, состояния)
|
||||||
|
|||||||
+124
-15
@@ -178,54 +178,162 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sprint 5 — Все блоки текущего сайта
|
## Sprint 5 — Все блоки текущего сайта ✅ ЗАВЕРШЁН
|
||||||
|
|
||||||
**Цель:** Задокументировать ВСЕ блоки страницы perm.oclinica.ru/lor за один спринт.
|
**Цель:** Задокументировать ВСЕ блоки страницы perm.oclinica.ru/lor за один спринт.
|
||||||
Источник: скриншот страницы + CSS сайта. Только фронтенд, mock-данные.
|
Источник: скриншот страницы + CSS сайта. Только фронтенд, mock-данные.
|
||||||
|
|
||||||
### Блоки с реального сайта (сверху вниз, по скриншоту /lor)
|
### Блоки с реального сайта (сверху вниз, по скриншоту /lor)
|
||||||
|
|
||||||
**Шапка и навигация** → `/components/navigation`
|
**Шапка и навигация** → `/blocks/navigation`
|
||||||
- [ ] FE: Топ-бар: адрес «Б. Цитная, 3», телефон /342/ 255-53-84, кнопка «Заказать звонок» (pill)
|
- [x] FE: Топ-бар: адрес «К. Цеткин, 9», телефон /342/ 255-53-84 (25px), кнопка «Заказать звонок» (pill)
|
||||||
- [ ] FE: Логотип «КЛИНИКА УХО ГОРЛО НОС ИМ. ПРОФ. Е.Н. ОЛЕНЕВОЙ»
|
- [x] FE: Логотип «КЛИНИКА УХО ГОРЛО НОС ИМ. ПРОФ. Е.Н. ОЛЕНЕВОЙ»
|
||||||
- [ ] FE: Главное меню: Клиника / ЛОР врачи / Заболевания / Вопрос-ответ / ЛОР-операции / Сурдология / Цены / Контакты
|
- [x] FE: Главное меню 18px: Клиника / ЛОР врачи / Заболевания / Вопрос-ответ / ЛОР операции / Сурдология / Цены / Контакты
|
||||||
|
- [x] FE: v1.1 — адрес, ссылки, размеры шрифтов, цвета синхронизированы с реальным сайтом
|
||||||
|
|
||||||
**Hero-баннер** → `/blocks/hero`
|
**Hero-баннер** → `/blocks/hero`
|
||||||
- [x] FE: Баннер — единый фон #f9f4e7 (светло-кремовый, замерян пикселем), галочки #bf9975
|
- [x] FE: Баннер — единый фон #f9f4e7 (светло-кремовый, замерян пикселем), галочки #bf9975
|
||||||
- [x] FE: Правая часть: реальное фото врача с пациентом (спарсено с сайта → public/hero-doctor.jpg)
|
- [x] FE: Правая часть: реальное фото врача с пациентом (спарсено с сайта → public/hero-doctor.jpg)
|
||||||
- [x] LLM: v1.1 — исправлен цвет фона, кнопка outline, галочки бежевые
|
- [x] LLM: v1.1 — исправлен цвет фона, кнопка outline, галочки бежевые
|
||||||
|
- [x] FE: v1.2 — H1: 36px/#cb9768, заголовок баннера: 22px/#333, CTA: pill-стиль, дефис в H1
|
||||||
|
- [x] FE: v1.3 — счётчик «Поделиться ✉ 98572», убраны кнопки VK/FB/TW
|
||||||
|
|
||||||
**Вводный текст (CEO-блок)** → `/blocks/ceo`
|
**Вводный текст (CEO-блок)** → `/blocks/ceo`
|
||||||
- [ ] FE: Текст специализации клиники, Q&A вопросы-стимулы
|
- [x] FE: Текст специализации клиники, Q&A вопросы-стимулы
|
||||||
|
- [x] FE: v1.1 — адрес «ул. Клары Цеткин, 9» (было «ул. Цитная, 9»), цвет ссылок #52b4bd
|
||||||
|
|
||||||
**Блок врачей** → `/blocks/doctors`
|
**Блок врачей** → `/blocks/doctors`
|
||||||
- [x] FE: Заголовок text-3xl + 3 стат-блока (без фона, #60959c + border-bottom) + сетка 6 карточек
|
- [x] FE: Заголовок text-3xl + 3 стат-блока (без фона, #60959c + border-bottom) + сетка 6 карточек
|
||||||
- [x] FE: Реальные фото 6 врачей (спарсены с сайта → public/doctors/)
|
- [x] FE: Реальные фото 6 врачей (спарсены с сайта → public/doctors/)
|
||||||
- [x] FE: Реальные имена и специализации врачей из слайдера /lor
|
- [x] FE: Реальные имена и специализации врачей из слайдера /lor
|
||||||
- [x] LLM: v1.1
|
- [x] LLM: v1.1
|
||||||
|
- [x] FE: v1.2 — H2: 36px/#000000, line-height 38px
|
||||||
|
|
||||||
**Блок отзывов** → `/blocks/reviews`
|
**Блок отзывов** → `/blocks/reviews`
|
||||||
- [ ] FE: Карусель: кавычка, текст отзыва, «Читать полностью», стрелки
|
- [x] FE: Карусель: кавычка, текст отзыва, «Читать полностью», стрелки
|
||||||
|
- [x] FE: v1.1 — H2: 36px/#000000, line-height 38px
|
||||||
|
|
||||||
**Формы записи** → `/blocks/contact-forms`
|
**Формы записи** → `/blocks/contact-forms`
|
||||||
- [ ] FE: Форма «Будьте здоровы!» (фон #b8e6ed, поля: имя/телефон/врач, кнопка «Запишите меня!»)
|
- [x] FE: Форма «Будьте здоровы!» (фон #d4f6f8, поля: имя/телефон/врач, кнопка «Запишите меня!»)
|
||||||
- [ ] FE: Форма «Узнайте стоимость операции» (белый фон, поля: имя/телефон, кнопка «Перезвоните мне»)
|
- [x] FE: Форма «Узнайте стоимость операции» (фон #d4f6f8, поля: имя/телефон, кнопка «Перезвоните мне»)
|
||||||
|
- [x] FE: v1.1 — H2: 36px/#000000, фон обеих форм: #d4f6f8 (ранее #b8e6ed и #fff)
|
||||||
|
- [x] FE: v1.2 — кнопка bb-btn-lg 18px bold (было bb-btn-md 14px), border-radius 4px (было 7px)
|
||||||
|
|
||||||
**Блок новостей** → `/blocks/news`
|
**Блок новостей** → `/blocks/news`
|
||||||
- [ ] FE: 4 карточки новостей в ряд (дата + заголовок), кнопка «Все новости» (mock)
|
- [x] FE: 4 карточки новостей в ряд (дата + заголовок), кнопка «Все новости» (mock)
|
||||||
|
- [x] FE: v1.1 — H2: 36px/#000000, фон секции: #f2fee6 (ранее белый)
|
||||||
|
|
||||||
**Footer (подвал)** → `/blocks/contact`
|
**Footer (подвал)** → `/blocks/contact`
|
||||||
- [ ] FE: 4 колонки ссылок, логотип, адрес, соцсети, часы работы
|
- [x] FE: 4 колонки ссылок по реальному сайту (О клинике 13, Заболевания 5, Вопрос-ответ 6, Операции 11)
|
||||||
|
- [x] FE: Два адреса: Клары Цеткин, 9 + Газеты Звезда, 31А
|
||||||
|
- [x] FE: Два графика работы по филиалам
|
||||||
|
- [x] FE: Соцсети: VK, YT, TG, OK, Дзен
|
||||||
|
- [x] FE: v1.1 — полное обновление контента по реальному сайту
|
||||||
|
|
||||||
### Общее к Sprint 5
|
### Общее к Sprint 5
|
||||||
- [x] FE: LLM-блоки на hero v1.1 и doctors v1.1
|
- [x] FE: LLM-блоки на hero v1.1 и doctors v1.1
|
||||||
- [x] Docs: Типографика сайта — реальные стили на 23.03.2026 (новый раздел в /foundation/typography)
|
- [x] Docs: Типографика сайта — реальные стили на 23.03.2026 (новый раздел в /foundation/typography)
|
||||||
- [x] Docs: Цвета — исправлен #f9f4e7 (Hero), #b8e6ed (форма), #e9e4d4 (пилюли)
|
- [x] Docs: Цвета — исправлен #f9f4e7 (Hero), #b8e6ed (форма), #e9e4d4 (пилюли)
|
||||||
- [ ] FE: Убрать `soon` у Hero и Doctors в Sidebar
|
- [x] FE: Убрать `soon` у Hero и Doctors в Sidebar
|
||||||
- [ ] FE: CEO-блок, отзывы, формы, новости, footer
|
- [x] FE: CEO-блок v1.1
|
||||||
- [ ] Docs: Финальное обновление `docs/LLM_CONTEXT.md` по итогам Sprint 5
|
- [x] FE: Блоки отзывов, форм записи, новостей — компоненты + страницы документации
|
||||||
|
- [x] FE: Сравнение ВСЕХ блоков с реальным сайтом и синхронизация (24.03.2026)
|
||||||
|
- [x] Docs: Обновление `docs/LLM_CONTEXT.md` v4.3
|
||||||
|
- [x] FE: Исправлены CSS-стили ВСЕХ блоков по реальному сайту (24.03.2026):
|
||||||
|
- H1 страницы: 36px, #cb9768 (ранее ~20px, #53514e)
|
||||||
|
- H2 секций: 36px, #000000 (ранее ~20-30px, #111827)
|
||||||
|
- Hero заголовок: 22px, #333 (ранее 16px, #111827)
|
||||||
|
- CTA-кнопка Hero: pill (ранее outline)
|
||||||
|
- Фон форм: #d4f6f8 (ранее #b8e6ed и #fff)
|
||||||
|
- Фон новостей: #f2fee6 (ранее #fff)
|
||||||
|
- Типографика сайта: обновлена таблица стилей
|
||||||
|
- [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 офлайн» заменена на серую точку
|
||||||
|
- [x] FE: Footer (блок `/blocks/contact` готов)
|
||||||
|
|
||||||
**Результат спринта (в работе):** Hero + Doctors задокументированы с реальными фото и точными цветами.
|
- [x] FE: CEO-блок v1.2 — убраны горизонтальные отступы (padding 40px 48px → 40px 0)
|
||||||
|
- [x] FE: Исправлен баг BlockMetaBar.saveFullVersion() — использовал defaultVersion вместо meta.version, что откатывало версию в БД при сохранении снапшота
|
||||||
|
|
||||||
|
**Результат спринта:** Hero v1.3, Doctors v1.2, CEO v1.2, Reviews v1.1, ContactForms v1.2, News v1.1, Navigation v1.3, Contact v1.1 — все блоки /lor задокументированы. Метаданные блоков хранятся в БД. Система снапшотов работает корректно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sprint 5.5 — «Просмотр текущей страницы» (внеочередной) ✅ ЗАВЕРШЁН
|
||||||
|
|
||||||
|
**Цель:** Добавить интерактивный раздел брендбука, который собирает главную страницу из уже задокументированных блоков.
|
||||||
|
Показывает живой превью того, как выглядит сайт на основе данных брендбука.
|
||||||
|
|
||||||
|
### Концепция UX
|
||||||
|
|
||||||
|
**Маршрут:** `/pages/preview`
|
||||||
|
**Сайдбар:** добавить в раздел «Страницы» с пометкой (если ещё нет блоков — показывает заглушку с кнопкой)
|
||||||
|
|
||||||
|
**Два состояния страницы:**
|
||||||
|
|
||||||
|
1. **Пустое состояние** (первый вход, или если превью не создавалось):
|
||||||
|
- Заголовок «Просмотр текущей страницы»
|
||||||
|
- Описание: «Здесь будет собрана главная страница из задокументированных блоков»
|
||||||
|
- Активная кнопка «Создать» (`.bb-btn bb-btn-primary`)
|
||||||
|
- После нажатия → переход в «созданное» состояние
|
||||||
|
|
||||||
|
2. **Созданное состояние** (после нажатия «Создать»):
|
||||||
|
- Превью главной страницы из всех доступных блоков в порядке сверху вниз, как на perm.oclinica.ru/lor
|
||||||
|
- Кнопка «Пересобрать» в шапке (сбрасывает до исходного состояния)
|
||||||
|
- Сборка только из блоков, у которых есть готовый компонент (не mock-заглушки)
|
||||||
|
- Блоки рендерятся как реальные React-компоненты внутри `<section>`
|
||||||
|
|
||||||
|
**Порядок блоков в превью** (по perm.oclinica.ru/lor, только готовые):
|
||||||
|
1. Hero-баннер (`/blocks/hero` → компонент HeroBlock)
|
||||||
|
2. Блок врачей (`/blocks/doctors` → компонент DoctorsBlock)
|
||||||
|
3. Блок отзывов (`/blocks/reviews` → когда будет готов)
|
||||||
|
4. Форма записи (`/blocks/contact-forms` → когда будет готова)
|
||||||
|
5. Блок новостей (`/blocks/news` → когда будет готов)
|
||||||
|
6. Footer (`/blocks/contact` → когда будет готов)
|
||||||
|
|
||||||
|
**Техническая реализация (FE only, без бэкенда):**
|
||||||
|
- Состояние сохраняется в `localStorage` (`preview-created: true/false`)
|
||||||
|
- Каждый задокументированный блок выносится в переиспользуемый React-компонент
|
||||||
|
- Страница `/pages/preview` импортирует компоненты и рендерит их в нужном порядке
|
||||||
|
- Блоки, которых ещё нет → показывается placeholder с текстом «Блок в разработке»
|
||||||
|
|
||||||
|
### Задачи
|
||||||
|
|
||||||
|
- [x] FE: Страница `/pages/preview` — пустое состояние с кнопкой «Создать»
|
||||||
|
- [x] FE: Логика `localStorage` — сохранение/сброс состояния превью
|
||||||
|
- [x] FE: Рефактор `/blocks/hero/page.tsx` — вынести баннер в компонент `HeroBlock` (переиспользуемый)
|
||||||
|
- [x] FE: Рефактор `/blocks/doctors/page.tsx` — вынести в компонент `DoctorsBlock`
|
||||||
|
- [x] FE: Placeholder-компонент для блоков, которые ещё не готовы (серая рамка с названием блока)
|
||||||
|
- [x] FE: Сборка превью: рендер всех доступных компонентов в порядке реального сайта
|
||||||
|
- [x] FE: Sidebar — добавить «Просмотр страницы» в раздел «Страницы»
|
||||||
|
- [x] FE: Кнопка «Пересобрать» в созданном состоянии
|
||||||
|
- [x] FE: Toggle «В превью» через localStorage (fallback при API офлайн)
|
||||||
|
- [x] BE: BlockMetaBar + PreviewClient подключены к NestJS API `/blocks`
|
||||||
|
- [x] BE: Метаданные блоков (version, changelog, isInPreview) в PostgreSQL
|
||||||
|
- [x] Docs: Добавить `/pages/preview` v1.0 в LLM_CONTEXT.md
|
||||||
|
- [x] FE: Кнопка «Сохранить версию» — фиксация version + changelog из кода в БД
|
||||||
|
- [x] BE: Модель BlockSnapshot (html, css, version, changelog) + миграция
|
||||||
|
- [x] BE: API эндпоинты: POST/GET `/blocks/snapshots`, GET `/blocks/snapshots/:id`
|
||||||
|
- [x] FE: BlockMetaBar — dropdown переключения между сохранёнными версиями
|
||||||
|
- [x] FE: HTML-снимок блока при сохранении версии (innerHTML + CSS capture)
|
||||||
|
- [x] FE: Все 8 страниц блоков — split на Server/Client Components для snapshot support
|
||||||
|
- [x] FE: Навигация v1.3 — подменю с hover-dropdown, navData.ts, обновлённые стили блоков
|
||||||
|
|
||||||
|
### Зависимости
|
||||||
|
- Зависит от: Sprint 5 (блоки hero и doctors уже готовы — ✅)
|
||||||
|
- По мере добавления новых блоков в Sprint 5 — они автоматически подключаются к превью
|
||||||
|
|
||||||
|
### Ожидаемый результат
|
||||||
|
- Раздел «Просмотр текущей страницы» работает в браузере
|
||||||
|
- Кнопка «Создать» собирает главную страницу из задокументированных блоков
|
||||||
|
- Отсутствующие блоки отображаются как плейсхолдеры
|
||||||
|
- Кнопка «Пересобрать» позволяет сбросить и пересоздать
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -355,6 +463,7 @@
|
|||||||
| 3 | Кнопки и форм-контролы | FE | CSS реального сайта |
|
| 3 | Кнопки и форм-контролы | FE | CSS реального сайта |
|
||||||
| 4 | Карточки, бейджи, алерты | FE | CSS реального сайта |
|
| 4 | Карточки, бейджи, алерты | FE | CSS реального сайта |
|
||||||
| 5 | ВСЕ блоки сайта | FE | Все блоки /lor, mock-данные |
|
| 5 | ВСЕ блоки сайта | FE | Все блоки /lor, mock-данные |
|
||||||
|
| 5.5 | Просмотр текущей страницы | FE | Кнопка «Создать», сборка из блоков, localStorage |
|
||||||
| 6 | Все страницы (сборки) | FE | Сборки из блоков, mock-данные |
|
| 6 | Все страницы (сборки) | FE | Сборки из блоков, mock-данные |
|
||||||
| 7 | Авторизация (viewer / editor) | BE + FE | JWT, роли, login-страница, шапка с именем |
|
| 7 | Авторизация (viewer / editor) | BE + FE | JWT, роли, login-страница, шапка с именем |
|
||||||
| 8 | Реальные данные | BE + FE | NestJS прокси → oclinica.ru, кэш 15 мин |
|
| 8 | Реальные данные | BE + FE | NestJS прокси → oclinica.ru, кэш 15 мин |
|
||||||
|
|||||||
Generated
+492
@@ -19,12 +19,21 @@ importers:
|
|||||||
'@nestjs/platform-express':
|
'@nestjs/platform-express':
|
||||||
specifier: ^11.0.1
|
specifier: ^11.0.1
|
||||||
version: 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)
|
version: 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)
|
||||||
|
'@prisma/adapter-pg':
|
||||||
|
specifier: ^7.5.0
|
||||||
|
version: 7.5.0
|
||||||
'@prisma/client':
|
'@prisma/client':
|
||||||
specifier: ^7.5.0
|
specifier: ^7.5.0
|
||||||
version: 7.5.0(prisma@7.5.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)
|
version: 7.5.0(prisma@7.5.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)
|
||||||
|
'@types/pg':
|
||||||
|
specifier: ^8.20.0
|
||||||
|
version: 8.20.0
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^17.3.1
|
specifier: ^17.3.1
|
||||||
version: 17.3.1
|
version: 17.3.1
|
||||||
|
pg:
|
||||||
|
specifier: ^8.20.0
|
||||||
|
version: 8.20.0
|
||||||
reflect-metadata:
|
reflect-metadata:
|
||||||
specifier: ^0.2.2
|
specifier: ^0.2.2
|
||||||
version: 0.2.2
|
version: 0.2.2
|
||||||
@@ -98,6 +107,9 @@ importers:
|
|||||||
tsconfig-paths:
|
tsconfig-paths:
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.21.0
|
||||||
|
version: 4.21.0
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.7.3
|
specifier: ^5.7.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -390,6 +402,162 @@ packages:
|
|||||||
'@emnapi/wasi-threads@1.2.0':
|
'@emnapi/wasi-threads@1.2.0':
|
||||||
resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
|
resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [aix]
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.27.4':
|
||||||
|
resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.27.4':
|
||||||
|
resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.27.4':
|
||||||
|
resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.27.4':
|
||||||
|
resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [mips64el]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.27.4':
|
||||||
|
resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-arm64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-arm64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/openharmony-arm64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openharmony]
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [sunos]
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.27.4':
|
||||||
|
resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.27.4':
|
||||||
|
resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.9.1':
|
'@eslint-community/eslint-utils@4.9.1':
|
||||||
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
|
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
@@ -1015,6 +1183,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
||||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||||
|
|
||||||
|
'@prisma/adapter-pg@7.5.0':
|
||||||
|
resolution: {integrity: sha512-EJx7OLULahcC3IjJgdx2qRDNCT+ToY2v66UkeETMCLhNOTgqVzRzYvOEphY7Zp0eHyzfkC33Edd/qqeadf9R4A==}
|
||||||
|
|
||||||
'@prisma/client-runtime-utils@7.5.0':
|
'@prisma/client-runtime-utils@7.5.0':
|
||||||
resolution: {integrity: sha512-KnJ2b4Si/pcWEtK68uM+h0h1oh80CZt2suhLTVuLaSKg4n58Q9jBF/A42Kw6Ma+aThy1yAhfDeTC0JvEmeZnFQ==}
|
resolution: {integrity: sha512-KnJ2b4Si/pcWEtK68uM+h0h1oh80CZt2suhLTVuLaSKg4n58Q9jBF/A42Kw6Ma+aThy1yAhfDeTC0JvEmeZnFQ==}
|
||||||
|
|
||||||
@@ -1042,6 +1213,9 @@ packages:
|
|||||||
'@prisma/dev@0.20.0':
|
'@prisma/dev@0.20.0':
|
||||||
resolution: {integrity: sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ==}
|
resolution: {integrity: sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ==}
|
||||||
|
|
||||||
|
'@prisma/driver-adapter-utils@7.5.0':
|
||||||
|
resolution: {integrity: sha512-B79N/amgV677mFesFDBAdrW0OIaqawap9E0sjgLBtzIz2R3hIMS1QB8mLZuUEiS4q5Y8Oh3I25Kw4SLxMypk9Q==}
|
||||||
|
|
||||||
'@prisma/engines-version@7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e':
|
'@prisma/engines-version@7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e':
|
||||||
resolution: {integrity: sha512-E+iRV/vbJLl8iGjVr6g/TEWokA+gjkV/doZkaQN1i/ULVdDwGnPJDfLUIFGS3BVwlG/m6L8T4x1x5isl8hGMxA==}
|
resolution: {integrity: sha512-E+iRV/vbJLl8iGjVr6g/TEWokA+gjkV/doZkaQN1i/ULVdDwGnPJDfLUIFGS3BVwlG/m6L8T4x1x5isl8hGMxA==}
|
||||||
|
|
||||||
@@ -1262,6 +1436,12 @@ packages:
|
|||||||
'@types/node@22.19.15':
|
'@types/node@22.19.15':
|
||||||
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
|
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
|
||||||
|
|
||||||
|
'@types/pg@8.11.11':
|
||||||
|
resolution: {integrity: sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==}
|
||||||
|
|
||||||
|
'@types/pg@8.20.0':
|
||||||
|
resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==}
|
||||||
|
|
||||||
'@types/qs@6.15.0':
|
'@types/qs@6.15.0':
|
||||||
resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==}
|
resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==}
|
||||||
|
|
||||||
@@ -2141,6 +2321,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
|
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
esbuild@0.27.4:
|
||||||
|
resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
escalade@3.2.0:
|
escalade@3.2.0:
|
||||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -3382,6 +3567,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
obuf@1.1.2:
|
||||||
|
resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
|
||||||
|
|
||||||
ohash@2.0.11:
|
ohash@2.0.11:
|
||||||
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
|
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
|
||||||
|
|
||||||
@@ -3479,6 +3667,48 @@ packages:
|
|||||||
perfect-debounce@1.0.0:
|
perfect-debounce@1.0.0:
|
||||||
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||||
|
|
||||||
|
pg-cloudflare@1.3.0:
|
||||||
|
resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==}
|
||||||
|
|
||||||
|
pg-connection-string@2.12.0:
|
||||||
|
resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==}
|
||||||
|
|
||||||
|
pg-int8@1.0.1:
|
||||||
|
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
||||||
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
|
pg-numeric@1.0.2:
|
||||||
|
resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
pg-pool@3.13.0:
|
||||||
|
resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==}
|
||||||
|
peerDependencies:
|
||||||
|
pg: '>=8.0'
|
||||||
|
|
||||||
|
pg-protocol@1.13.0:
|
||||||
|
resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==}
|
||||||
|
|
||||||
|
pg-types@2.2.0:
|
||||||
|
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
pg-types@4.1.0:
|
||||||
|
resolution: {integrity: sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
pg@8.20.0:
|
||||||
|
resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==}
|
||||||
|
engines: {node: '>= 16.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
pg-native: '>=3.0.1'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
pg-native:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
pgpass@1.0.5:
|
||||||
|
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -3521,6 +3751,41 @@ packages:
|
|||||||
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
|
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
postgres-array@2.0.0:
|
||||||
|
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
postgres-array@3.0.4:
|
||||||
|
resolution: {integrity: sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
postgres-bytea@1.0.1:
|
||||||
|
resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
postgres-bytea@3.0.0:
|
||||||
|
resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
postgres-date@1.0.7:
|
||||||
|
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
postgres-date@2.1.0:
|
||||||
|
resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
postgres-interval@1.2.0:
|
||||||
|
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
postgres-interval@3.0.0:
|
||||||
|
resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
postgres-range@1.1.4:
|
||||||
|
resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==}
|
||||||
|
|
||||||
postgres@3.4.7:
|
postgres@3.4.7:
|
||||||
resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==}
|
resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -3816,6 +4081,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
|
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
|
||||||
engines: {node: '>= 12'}
|
engines: {node: '>= 12'}
|
||||||
|
|
||||||
|
split2@4.2.0:
|
||||||
|
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||||
|
engines: {node: '>= 10.x'}
|
||||||
|
|
||||||
sprintf-js@1.0.3:
|
sprintf-js@1.0.3:
|
||||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||||
|
|
||||||
@@ -4075,6 +4344,11 @@ packages:
|
|||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
|
tsx@4.21.0:
|
||||||
|
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -4267,6 +4541,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
|
resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
|
||||||
|
xtend@4.0.2:
|
||||||
|
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||||
|
engines: {node: '>=0.4'}
|
||||||
|
|
||||||
y18n@5.0.8:
|
y18n@5.0.8:
|
||||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -4603,6 +4881,84 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-arm64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-arm64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openharmony-arm64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.27.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))':
|
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 9.39.4(jiti@2.6.1)
|
eslint: 9.39.4(jiti@2.6.1)
|
||||||
@@ -5282,6 +5638,15 @@ snapshots:
|
|||||||
|
|
||||||
'@pkgr/core@0.2.9': {}
|
'@pkgr/core@0.2.9': {}
|
||||||
|
|
||||||
|
'@prisma/adapter-pg@7.5.0':
|
||||||
|
dependencies:
|
||||||
|
'@prisma/driver-adapter-utils': 7.5.0
|
||||||
|
'@types/pg': 8.11.11
|
||||||
|
pg: 8.20.0
|
||||||
|
postgres-array: 3.0.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- pg-native
|
||||||
|
|
||||||
'@prisma/client-runtime-utils@7.5.0': {}
|
'@prisma/client-runtime-utils@7.5.0': {}
|
||||||
|
|
||||||
'@prisma/client@7.5.0(prisma@7.5.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)':
|
'@prisma/client@7.5.0(prisma@7.5.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)':
|
||||||
@@ -5326,6 +5691,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
|
'@prisma/driver-adapter-utils@7.5.0':
|
||||||
|
dependencies:
|
||||||
|
'@prisma/debug': 7.5.0
|
||||||
|
|
||||||
'@prisma/engines-version@7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e': {}
|
'@prisma/engines-version@7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e': {}
|
||||||
|
|
||||||
'@prisma/engines@7.5.0':
|
'@prisma/engines@7.5.0':
|
||||||
@@ -5554,6 +5923,18 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
|
'@types/pg@8.11.11':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 22.19.15
|
||||||
|
pg-protocol: 1.13.0
|
||||||
|
pg-types: 4.1.0
|
||||||
|
|
||||||
|
'@types/pg@8.20.0':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 22.19.15
|
||||||
|
pg-protocol: 1.13.0
|
||||||
|
pg-types: 2.2.0
|
||||||
|
|
||||||
'@types/qs@6.15.0': {}
|
'@types/qs@6.15.0': {}
|
||||||
|
|
||||||
'@types/range-parser@1.2.7': {}
|
'@types/range-parser@1.2.7': {}
|
||||||
@@ -6530,6 +6911,35 @@ snapshots:
|
|||||||
is-date-object: 1.1.0
|
is-date-object: 1.1.0
|
||||||
is-symbol: 1.1.1
|
is-symbol: 1.1.1
|
||||||
|
|
||||||
|
esbuild@0.27.4:
|
||||||
|
optionalDependencies:
|
||||||
|
'@esbuild/aix-ppc64': 0.27.4
|
||||||
|
'@esbuild/android-arm': 0.27.4
|
||||||
|
'@esbuild/android-arm64': 0.27.4
|
||||||
|
'@esbuild/android-x64': 0.27.4
|
||||||
|
'@esbuild/darwin-arm64': 0.27.4
|
||||||
|
'@esbuild/darwin-x64': 0.27.4
|
||||||
|
'@esbuild/freebsd-arm64': 0.27.4
|
||||||
|
'@esbuild/freebsd-x64': 0.27.4
|
||||||
|
'@esbuild/linux-arm': 0.27.4
|
||||||
|
'@esbuild/linux-arm64': 0.27.4
|
||||||
|
'@esbuild/linux-ia32': 0.27.4
|
||||||
|
'@esbuild/linux-loong64': 0.27.4
|
||||||
|
'@esbuild/linux-mips64el': 0.27.4
|
||||||
|
'@esbuild/linux-ppc64': 0.27.4
|
||||||
|
'@esbuild/linux-riscv64': 0.27.4
|
||||||
|
'@esbuild/linux-s390x': 0.27.4
|
||||||
|
'@esbuild/linux-x64': 0.27.4
|
||||||
|
'@esbuild/netbsd-arm64': 0.27.4
|
||||||
|
'@esbuild/netbsd-x64': 0.27.4
|
||||||
|
'@esbuild/openbsd-arm64': 0.27.4
|
||||||
|
'@esbuild/openbsd-x64': 0.27.4
|
||||||
|
'@esbuild/openharmony-arm64': 0.27.4
|
||||||
|
'@esbuild/sunos-x64': 0.27.4
|
||||||
|
'@esbuild/win32-arm64': 0.27.4
|
||||||
|
'@esbuild/win32-ia32': 0.27.4
|
||||||
|
'@esbuild/win32-x64': 0.27.4
|
||||||
|
|
||||||
escalade@3.2.0: {}
|
escalade@3.2.0: {}
|
||||||
|
|
||||||
escape-html@1.0.3: {}
|
escape-html@1.0.3: {}
|
||||||
@@ -8033,6 +8443,8 @@ snapshots:
|
|||||||
define-properties: 1.2.1
|
define-properties: 1.2.1
|
||||||
es-object-atoms: 1.1.1
|
es-object-atoms: 1.1.1
|
||||||
|
|
||||||
|
obuf@1.1.2: {}
|
||||||
|
|
||||||
ohash@2.0.11: {}
|
ohash@2.0.11: {}
|
||||||
|
|
||||||
on-finished@2.4.1:
|
on-finished@2.4.1:
|
||||||
@@ -8133,6 +8545,53 @@ snapshots:
|
|||||||
|
|
||||||
perfect-debounce@1.0.0: {}
|
perfect-debounce@1.0.0: {}
|
||||||
|
|
||||||
|
pg-cloudflare@1.3.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
pg-connection-string@2.12.0: {}
|
||||||
|
|
||||||
|
pg-int8@1.0.1: {}
|
||||||
|
|
||||||
|
pg-numeric@1.0.2: {}
|
||||||
|
|
||||||
|
pg-pool@3.13.0(pg@8.20.0):
|
||||||
|
dependencies:
|
||||||
|
pg: 8.20.0
|
||||||
|
|
||||||
|
pg-protocol@1.13.0: {}
|
||||||
|
|
||||||
|
pg-types@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
pg-int8: 1.0.1
|
||||||
|
postgres-array: 2.0.0
|
||||||
|
postgres-bytea: 1.0.1
|
||||||
|
postgres-date: 1.0.7
|
||||||
|
postgres-interval: 1.2.0
|
||||||
|
|
||||||
|
pg-types@4.1.0:
|
||||||
|
dependencies:
|
||||||
|
pg-int8: 1.0.1
|
||||||
|
pg-numeric: 1.0.2
|
||||||
|
postgres-array: 3.0.4
|
||||||
|
postgres-bytea: 3.0.0
|
||||||
|
postgres-date: 2.1.0
|
||||||
|
postgres-interval: 3.0.0
|
||||||
|
postgres-range: 1.1.4
|
||||||
|
|
||||||
|
pg@8.20.0:
|
||||||
|
dependencies:
|
||||||
|
pg-connection-string: 2.12.0
|
||||||
|
pg-pool: 3.13.0(pg@8.20.0)
|
||||||
|
pg-protocol: 1.13.0
|
||||||
|
pg-types: 2.2.0
|
||||||
|
pgpass: 1.0.5
|
||||||
|
optionalDependencies:
|
||||||
|
pg-cloudflare: 1.3.0
|
||||||
|
|
||||||
|
pgpass@1.0.5:
|
||||||
|
dependencies:
|
||||||
|
split2: 4.2.0
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@2.3.1: {}
|
picomatch@2.3.1: {}
|
||||||
@@ -8169,6 +8628,28 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
postgres-array@2.0.0: {}
|
||||||
|
|
||||||
|
postgres-array@3.0.4: {}
|
||||||
|
|
||||||
|
postgres-bytea@1.0.1: {}
|
||||||
|
|
||||||
|
postgres-bytea@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
obuf: 1.1.2
|
||||||
|
|
||||||
|
postgres-date@1.0.7: {}
|
||||||
|
|
||||||
|
postgres-date@2.1.0: {}
|
||||||
|
|
||||||
|
postgres-interval@1.2.0:
|
||||||
|
dependencies:
|
||||||
|
xtend: 4.0.2
|
||||||
|
|
||||||
|
postgres-interval@3.0.0: {}
|
||||||
|
|
||||||
|
postgres-range@1.1.4: {}
|
||||||
|
|
||||||
postgres@3.4.7: {}
|
postgres@3.4.7: {}
|
||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
@@ -8532,6 +9013,8 @@ snapshots:
|
|||||||
|
|
||||||
source-map@0.7.6: {}
|
source-map@0.7.6: {}
|
||||||
|
|
||||||
|
split2@4.2.0: {}
|
||||||
|
|
||||||
sprintf-js@1.0.3: {}
|
sprintf-js@1.0.3: {}
|
||||||
|
|
||||||
sqlstring@2.3.3: {}
|
sqlstring@2.3.3: {}
|
||||||
@@ -8809,6 +9292,13 @@ snapshots:
|
|||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
|
tsx@4.21.0:
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.27.4
|
||||||
|
get-tsconfig: 4.13.6
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
@@ -9073,6 +9563,8 @@ snapshots:
|
|||||||
imurmurhash: 0.1.4
|
imurmurhash: 0.1.4
|
||||||
signal-exit: 4.1.0
|
signal-exit: 4.1.0
|
||||||
|
|
||||||
|
xtend@4.0.2: {}
|
||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"buildCommand": "pnpm --filter web build",
|
||||||
|
"outputDirectory": "apps/web/.next",
|
||||||
|
"installCommand": "pnpm install"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user