diff --git a/apps/api/package.json b/apps/api/package.json
index 19baef4..2d20c24 100644
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -24,8 +24,11 @@
"@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
+ "@prisma/adapter-pg": "^7.5.0",
"@prisma/client": "^7.5.0",
+ "@types/pg": "^8.20.0",
"dotenv": "^17.3.1",
+ "pg": "^8.20.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
@@ -52,9 +55,13 @@
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
+ "tsx": "^4.21.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
+ "prisma": {
+ "seed": "ts-node prisma/seed.ts"
+ },
"jest": {
"moduleFileExtensions": [
"js",
diff --git a/apps/api/prisma.config.ts b/apps/api/prisma.config.ts
index 831a20f..846b38d 100644
--- a/apps/api/prisma.config.ts
+++ b/apps/api/prisma.config.ts
@@ -7,6 +7,7 @@ export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
+ seed: "tsx prisma/seed.ts",
},
datasource: {
url: process.env["DATABASE_URL"],
diff --git a/apps/api/prisma/migrations/20260323120441_add_block_model/migration.sql b/apps/api/prisma/migrations/20260323120441_add_block_model/migration.sql
new file mode 100644
index 0000000..92c14d8
--- /dev/null
+++ b/apps/api/prisma/migrations/20260323120441_add_block_model/migration.sql
@@ -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;
diff --git a/apps/api/prisma/migrations/migration_lock.toml b/apps/api/prisma/migrations/migration_lock.toml
new file mode 100644
index 0000000..044d57c
--- /dev/null
+++ b/apps/api/prisma/migrations/migration_lock.toml
@@ -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"
diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma
index 3d7590e..d186fcb 100644
--- a/apps/api/prisma/schema.prisma
+++ b/apps/api/prisma/schema.prisma
@@ -1,6 +1,5 @@
generator client {
- provider = "prisma-client"
- output = "../generated/prisma"
+ provider = "prisma-client-js"
}
datasource db {
@@ -40,3 +39,13 @@ model ExperimentalComponent {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
+
+model Block {
+ id String @id @default(uuid())
+ path String @unique
+ name String
+ version String
+ isInPreview Boolean @default(false)
+ updatedAt DateTime @updatedAt
+ createdAt DateTime @default(now())
+}
diff --git a/apps/api/prisma/seed.ts b/apps/api/prisma/seed.ts
new file mode 100644
index 0000000..960a2a9
--- /dev/null
+++ b/apps/api/prisma/seed.ts
@@ -0,0 +1,35 @@
+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: '/components/navigation', name: 'Шапка / Навигация', version: 'v1.0', isInPreview: true },
+ { path: '/blocks/hero', name: 'Hero-баннер', version: 'v1.1', isInPreview: true },
+ { path: '/blocks/ceo', name: 'Вводный текст (CEO-блок)', version: 'v0.1', isInPreview: false },
+ { path: '/blocks/doctors', name: 'Наши врачи', version: 'v1.1', isInPreview: true },
+ { path: '/blocks/reviews', name: 'Отзывы', version: 'v0.1', isInPreview: false },
+ { path: '/blocks/contact-forms', name: 'Формы записи', version: 'v0.1', isInPreview: false },
+ { path: '/blocks/news', name: 'Новости', version: 'v0.1', isInPreview: false },
+ { path: '/blocks/contact', name: 'Подвал / Контакт', version: 'v0.1', isInPreview: false },
+];
+
+async function main() {
+ for (const block of BLOCKS) {
+ await prisma.block.upsert({
+ where: { path: block.path },
+ update: {},
+ create: block,
+ });
+ }
+ console.log(`Seeded ${BLOCKS.length} blocks`);
+}
+
+main()
+ .catch((e) => {
+ console.error(e);
+ process.exit(1);
+ })
+ .finally(() => prisma.$disconnect());
diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts
index 8662803..50286d5 100644
--- a/apps/api/src/app.module.ts
+++ b/apps/api/src/app.module.ts
@@ -1,9 +1,10 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
+import { BlocksModule } from './blocks/blocks.module';
@Module({
- imports: [],
+ imports: [BlocksModule],
controllers: [AppController],
providers: [AppService],
})
diff --git a/apps/api/src/blocks/blocks.controller.ts b/apps/api/src/blocks/blocks.controller.ts
new file mode 100644
index 0000000..c979fb0
--- /dev/null
+++ b/apps/api/src/blocks/blocks.controller.ts
@@ -0,0 +1,25 @@
+import { Controller, Get, Patch, 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 },
+ ) {
+ return this.blocks.update(path, body);
+ }
+}
diff --git a/apps/api/src/blocks/blocks.module.ts b/apps/api/src/blocks/blocks.module.ts
new file mode 100644
index 0000000..e7b3d24
--- /dev/null
+++ b/apps/api/src/blocks/blocks.module.ts
@@ -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 {}
diff --git a/apps/api/src/blocks/blocks.service.ts b/apps/api/src/blocks/blocks.service.ts
new file mode 100644
index 0000000..bae1ea7
--- /dev/null
+++ b/apps/api/src/blocks/blocks.service.ts
@@ -0,0 +1,23 @@
+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 }) {
+ return this.prisma.block.update({ where: { path }, data });
+ }
+}
diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts
index f76bc8d..a7ce6d1 100644
--- a/apps/api/src/main.ts
+++ b/apps/api/src/main.ts
@@ -1,8 +1,10 @@
+import 'dotenv/config';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
+ app.enableCors({ origin: 'http://localhost:3001' });
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
diff --git a/apps/api/src/prisma/prisma.service.ts b/apps/api/src/prisma/prisma.service.ts
new file mode 100644
index 0000000..b963c06
--- /dev/null
+++ b/apps/api/src/prisma/prisma.service.ts
@@ -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();
+ }
+}
diff --git a/apps/web/app/blocks/ceo/page.tsx b/apps/web/app/blocks/ceo/page.tsx
index c71bf22..c35fa59 100644
--- a/apps/web/app/blocks/ceo/page.tsx
+++ b/apps/web/app/blocks/ceo/page.tsx
@@ -1,5 +1,7 @@
import type { Metadata } from "next";
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
+import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
+import { CeoBlock, CEO_QUESTIONS } from "@/components/blocks/CeoBlock";
export const metadata: Metadata = {
title: "Вводный текст (CEO-блок). Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
@@ -46,13 +48,6 @@ const LLM_CEO_TEXT = `
✕ Не менять стиль вопросов на другой формат
`.trim();
-const CEO_QUESTIONS = [
- "У вас болит ухо, заложен нос, першит в горле, и вы не можете понять причину?",
- "Вам срочно нужен платный ЛОР в Перми или, как ещё говорят, «ухогорлонос»?",
- "Заболел ребёнок?",
- "Срочно ищете частные ЛОР-клиники Перми для детей 0+ и взрослых с удобным режимом работы с 9:00 до 21:00 по будням?",
- "Вам назначили проведение эндоскопической операции на ухе, горле или носе?",
-];
export default function CeoPage() {
return (
@@ -68,6 +63,7 @@ export default function CeoPage() {
Вводный текст (CEO-блок)
+
Блок после hero-баннера на perm.oclinica.ru/lor. Описание специализации клиники
+ вопросы-стимулы для пациентов.
@@ -79,38 +75,8 @@ export default function CeoPage() {
- Обращайтесь в ЛОР центр ухо, горло, нос в Перми, наши врачи оториноларингологи обязательно Вам помогут!
-
-
- Клиника ЛОР болезней ухо, горло, нос – это наиболее современный центр оториноларингологии в Перми,
- благодаря эндоскопическому оборудованию, высокому профессионализму оториноларингологов.
-
-
Будьте здоровы!
+
+
diff --git a/apps/web/app/blocks/contact-forms/page.tsx b/apps/web/app/blocks/contact-forms/page.tsx
index e9f2384..b4506f9 100644
--- a/apps/web/app/blocks/contact-forms/page.tsx
+++ b/apps/web/app/blocks/contact-forms/page.tsx
@@ -1,5 +1,7 @@
import type { Metadata } from "next";
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
+import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
+import { ContactFormsBlock } from "@/components/blocks/ContactFormsBlock";
export const metadata: Metadata = {
title: "Формы записи. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
@@ -62,104 +64,18 @@ export default function ContactFormsPage() {
Формы записи
+
Два блока форм с perm.oclinica.ru/lor — запись на приём и запрос стоимости операции.
- {/* Форма 1: Будьте здоровы */}
+ {/* Живой пример */}
- Форма 1 — «Будьте здоровы!»
+ Живой пример
-
- Фон секции: #b8e6ed. Запись на приём к врачу.
-
-
-
-
- Будьте здоровы!
-
-
- Запишитесь на приём к врачу!
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Форма 2: Узнайте стоимость */}
-
-
- Форма 2 — «Узнайте стоимость операции»
-
-
- Фон секции: #ffffff. Запрос консультации по стоимости.
-
-
-
-
- Узнайте стоимость операции
-
-
- Проконсультируйтесь с ассистентом хирурга
-
-
-
-
-
-
-
-
-
+
{/* Сравнение двух форм */}
diff --git a/apps/web/app/blocks/contact/page.tsx b/apps/web/app/blocks/contact/page.tsx
index c982f2f..2a28c02 100644
--- a/apps/web/app/blocks/contact/page.tsx
+++ b/apps/web/app/blocks/contact/page.tsx
@@ -1,5 +1,7 @@
import type { Metadata } from "next";
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
+import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
+import { FooterBlock } from "@/components/blocks/FooterBlock";
export const metadata: Metadata = {
title: "Подвал (Footer). Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
@@ -82,6 +84,7 @@ export default function ContactFooterPage() {
Подвал (Footer)
+
Подвал сайта с perm.oclinica.ru — 4 колонки ссылок, логотип, адрес, часы работы, соцсети.
@@ -92,97 +95,7 @@ export default function ContactFooterPage() {
+
{/* Колонки */}
diff --git a/apps/web/app/blocks/doctors/page.tsx b/apps/web/app/blocks/doctors/page.tsx
index 5f8156a..e18d8de 100644
--- a/apps/web/app/blocks/doctors/page.tsx
+++ b/apps/web/app/blocks/doctors/page.tsx
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
import { DoctorsBlock, STATS, DOCTORS } from "@/components/blocks/DoctorsBlock";
+import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
export const metadata: Metadata = {
title: "Блок «Наши врачи». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
@@ -58,6 +59,7 @@ export default function DoctorsBlockPage() {
Блок «Наши врачи»
+
Блок на странице perm.oclinica.ru/lor — заголовок, 3 стат-блока, сетка из 6 карточек врачей.
diff --git a/apps/web/app/blocks/hero/page.tsx b/apps/web/app/blocks/hero/page.tsx
index 6f04400..b52e949 100644
--- a/apps/web/app/blocks/hero/page.tsx
+++ b/apps/web/app/blocks/hero/page.tsx
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
import { HeroBlock, HERO_CHECKS } from "@/components/blocks/HeroBlock";
+import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
export const metadata: Metadata = {
title: "Hero-баннер. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
@@ -63,6 +64,7 @@ export default function HeroPage() {
Hero-баннер
+
Главный баннер страницы раздела ЛОР — perm.oclinica.ru/lor. Двухколоночный блок, единый светло-кремовый фон{" "}
#f9f4e7.
diff --git a/apps/web/app/blocks/news/page.tsx b/apps/web/app/blocks/news/page.tsx
index 52c7efe..cc434eb 100644
--- a/apps/web/app/blocks/news/page.tsx
+++ b/apps/web/app/blocks/news/page.tsx
@@ -1,5 +1,7 @@
import type { Metadata } from "next";
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
+import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
+import { NewsBlock } from "@/components/blocks/NewsBlock";
export const metadata: Metadata = {
title: "Блок «Новости». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
@@ -79,6 +81,7 @@ export default function NewsBlockPage() {
Блок «Новости»
+
Блок новостей с perm.oclinica.ru/lor — 4 карточки в ряд (дата + заголовок-ссылка),
кнопка «Все новости».
@@ -90,42 +93,7 @@ export default function NewsBlockPage() {
+
{/* Анатомия карточки */}
diff --git a/apps/web/app/blocks/reviews/page.tsx b/apps/web/app/blocks/reviews/page.tsx
index d415536..f213335 100644
--- a/apps/web/app/blocks/reviews/page.tsx
+++ b/apps/web/app/blocks/reviews/page.tsx
@@ -1,5 +1,7 @@
import type { Metadata } from "next";
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
+import { BlockMetaBar } from "@/components/ui/BlockMetaBar";
+import { ReviewsBlock } from "@/components/blocks/ReviewsBlock";
export const metadata: Metadata = {
title: "Блок «Отзывы». Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
@@ -64,6 +66,7 @@ export default function ReviewsBlockPage() {
Блок «Отзывы о нас»
+
Карусель отзывов с perm.oclinica.ru/lor — большая кавычка, текст, «Читать полностью», стрелки.
@@ -74,76 +77,7 @@ export default function ReviewsBlockPage() {
Живой пример
-
- {/* Заголовок */}
-
-
- Отзывы о нас
-
-
- За 12 лет работы наши врачи оториноларингологи избавили от болезней ухо, горло, носа
- более 50 000 пациентов. Но даже сейчас мы высоко ценим каждый положительный отзыв
- и искренние слова благодарности.
-