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() {

Живой пример

-
-

- Клиника ухо, нос специализируется на оториноларингологии – лечении взрослых и детей - с ЛОР заболеваниями. ЛОР клиника представлена на двух адресах:{" "} - ул. Цитная, 9, ул. Г. Звезда, 31а.{" "} - Это Клиника лечения кашля и аллергии. -

- -
- {CEO_QUESTIONS.map((q) => ( -

— {q}

- ))} -
- -

- Обращайтесь в ЛОР центр ухо, горло, нос в Перми, наши врачи оториноларингологи обязательно Вам помогут! -

-

- Клиника ЛОР болезней ухо, горло, нос – это наиболее современный центр оториноларингологии в Перми, - благодаря эндоскопическому оборудованию, высокому профессионализму оториноларингологов. -

-

Будьте здоровы!

+
+
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() {

Живой пример

-
- {/* 4 колонки ссылок */} -
- {FOOTER_COLUMNS.map((col) => ( -
-

- {col.title} -

- -
- ))} -
- - {/* Нижняя полоса */} -
- {/* Логотип */} -
-
- ✚ -
-
-
- Клиника
ухо, горло, нос -
-
- им. проф. Е.Н.Оленевой -
-
-
- - {/* Адрес и соцсети */} -
-

- Мы находимся по адресу: Пермь, ул. Г. Звезда, 31а -

-
- {["VK", "OK", "YT", "TG"].map((s) => ( - - {s} - - ))} -
-
- - {/* Часы работы */} -
-

- Часы работы: -

-

Пн–пт: 9:00–21:00

-

Сб: 9:00–18:00

-

Вс: выходной

-
-
-
+ {/* Колонки */} 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() {

Живой пример

-
-

- Новости -

-
- {MOCK_NEWS.map((n) => ( -
-

- {n.date} -

- - {n.title} - -
- ))} -
-
- -
-
+ {/* Анатомия карточки */} 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 пациентов. Но даже сейчас мы высоко ценим каждый положительный отзыв - и искренние слова благодарности. -

-
- - {/* Карусель (статичная) */} -
- {/* Стрелка влево */} - - - {/* Карточка отзыва */} -
- {/* Декоративная кавычка */} -
- « -
-

- {MOCK_REVIEWS[0].text} -

- - Читать отзыв полностью - -
- - {/* Стрелка вправо */} - -
-
+ {/* Несколько примеров */} diff --git a/apps/web/app/components/buttons/page.tsx b/apps/web/app/components/buttons/page.tsx index 8490870..ce35a87 100644 --- a/apps/web/app/components/buttons/page.tsx +++ b/apps/web/app/components/buttons/page.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Button } from "@/components/ui/Button"; import { CodeCopy } from "@/components/ui/CodeCopy"; import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; +import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; export const metadata: Metadata = { title: "Кнопки. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", @@ -227,6 +228,7 @@ a.callback_url {

Кнопки

+

Кнопки скопированы с реального сайта{" "} diff --git a/apps/web/app/components/cards/page.tsx b/apps/web/app/components/cards/page.tsx index 26ca497..e22879b 100644 --- a/apps/web/app/components/cards/page.tsx +++ b/apps/web/app/components/cards/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { CodeCopy } from "@/components/ui/CodeCopy"; import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; +import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; export const metadata: Metadata = { title: "Карточки. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", @@ -417,6 +418,7 @@ export default function CardsPage() {

Карточки

+

Карточки врача, новости, отзыва, цены и услуги — основные блоки контента сайта. Бейджи, теги и алерты — вспомогательные элементы. diff --git a/apps/web/app/components/forms/page.tsx b/apps/web/app/components/forms/page.tsx index cd6376d..6bf27bb 100644 --- a/apps/web/app/components/forms/page.tsx +++ b/apps/web/app/components/forms/page.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Toggle } from "@/components/ui/Toggle"; import { CodeCopy } from "@/components/ui/CodeCopy"; import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; +import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; export const metadata: Metadata = { title: "Форм-контролы. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", @@ -264,6 +265,7 @@ input[type=email] {

Форм-контролы

+

Элементы ввода данных: текстовые поля, выпадающие списки, флажки, переключатели. Применяются в формах записи, фильтрах и настройках. diff --git a/apps/web/app/components/navigation/page.tsx b/apps/web/app/components/navigation/page.tsx index fc1b7a2..ab37eec 100644 --- a/apps/web/app/components/navigation/page.tsx +++ b/apps/web/app/components/navigation/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock"; import { NavigationBlock, NAV_ITEMS } from "@/components/blocks/NavigationBlock"; +import { BlockMetaBar } from "@/components/ui/BlockMetaBar"; export const metadata: Metadata = { title: "Шапка и навигация. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой", @@ -57,6 +58,7 @@ export default function NavigationPage() {

Шапка и навигация

+

Точное воспроизведение шапки с perm.oclinica.ru/lor. Три зоны: топ-бар, логотип, главное меню.

diff --git a/apps/web/app/pages/preview/PreviewClient.tsx b/apps/web/app/pages/preview/PreviewClient.tsx index fa494b3..e9dd22d 100644 --- a/apps/web/app/pages/preview/PreviewClient.tsx +++ b/apps/web/app/pages/preview/PreviewClient.tsx @@ -4,6 +4,11 @@ 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"; @@ -30,71 +35,89 @@ function BlockPlaceholder({ name, href }: { name: string; href: string }) { ); } -const BLOCKS: Array<{ +interface BlockDef { id: string; name: string; href: string; - ready: boolean; + path: string; + defaultReady: boolean; component?: React.ReactNode; -}> = [ +} + +const BLOCKS: BlockDef[] = [ { id: "navigation", name: "Шапка / Навигация", href: "/components/navigation", - ready: true, + path: "/components/navigation", + defaultReady: true, component: , }, { id: "hero", name: "Hero-баннер", href: "/blocks/hero", - ready: true, + path: "/blocks/hero", + defaultReady: true, component: , }, { id: "ceo", name: "Вводный текст (CEO-блок)", href: "/blocks/ceo", - ready: false, + path: "/blocks/ceo", + defaultReady: false, + component: , }, { id: "doctors", name: "Наши врачи", href: "/blocks/doctors", - ready: true, + path: "/blocks/doctors", + defaultReady: true, component: , }, { id: "reviews", name: "Отзывы", href: "/blocks/reviews", - ready: false, + path: "/blocks/reviews", + defaultReady: false, + component: , }, { id: "contact-forms", name: "Формы записи", href: "/blocks/contact-forms", - ready: false, + path: "/blocks/contact-forms", + defaultReady: false, + component: , }, { id: "news", name: "Новости", href: "/blocks/news", - ready: false, + path: "/blocks/news", + defaultReady: false, + component: , }, { id: "footer", name: "Подвал / Контакт", href: "/blocks/contact", - ready: false, + path: "/blocks/contact", + defaultReady: false, + component: , }, ]; -const READY_COUNT = BLOCKS.filter((b) => b.ready).length; - 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 | null>(null); + + const apiUrl = process.env.NEXT_PUBLIC_API_URL; useEffect(() => { setMounted(true); @@ -103,6 +126,27 @@ export function PreviewClient() { } }, []); + useEffect(() => { + if (!apiUrl) return; + fetch(`${apiUrl}/blocks`) + .then((r) => r.json()) + .then((data: Array<{ path: string; isInPreview: boolean }>) => { + const map: Record = {}; + 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 { + if (apiMeta !== null && block.path in apiMeta) { + return apiMeta[block.path] && !!block.component; + } + return block.defaultReady && !!block.component; + } + function handleCreate() { localStorage.setItem(STORAGE_KEY, "true"); setCreated(true); @@ -113,9 +157,10 @@ export function PreviewClient() { setCreated(false); } - // Avoid hydration mismatch — render nothing until mounted if (!mounted) return null; + const readyCount = BLOCKS.filter(isReady).length; + /* ── ПУСТОЕ СОСТОЯНИЕ ── */ if (!created) { return ( @@ -147,11 +192,11 @@ export function PreviewClient() { style={{ background: "#22c55e" }} /> - Готово блоков: {READY_COUNT} из {BLOCKS.length} + Готово блоков: {readyCount} из {BLOCKS.length} · - {BLOCKS.length - READY_COUNT} плейсхолдеров + {BLOCKS.length - readyCount} плейсхолдеров
@@ -183,7 +228,10 @@ export function PreviewClient() { Просмотр текущей страницы

- perm.oclinica.ru/lor · {READY_COUNT}/{BLOCKS.length} блоков готово + perm.oclinica.ru/lor · {readyCount}/{BLOCKS.length} блоков готово + {apiMeta === null && apiUrl && ( + · API загружается... + )}

+ + + + {/* Форма 2: Узнайте стоимость */} +
+
+

+ Узнайте стоимость операции +

+

+ Проконсультируйтесь с ассистентом хирурга +

+
+
+ + + + +
+
+ + ); +} diff --git a/apps/web/components/blocks/FooterBlock.tsx b/apps/web/components/blocks/FooterBlock.tsx new file mode 100644 index 0000000..085fbea --- /dev/null +++ b/apps/web/components/blocks/FooterBlock.tsx @@ -0,0 +1,119 @@ +const FOOTER_COLUMNS = [ + { + title: "О клинике", + links: ["Лицензия", "Миссия", "Врачи", "Вакансии", "История", "Образовательная деятельность", "При инфо"], + }, + { + title: "Заболевания", + links: ["Ринит", "Отит", "Гайморит", "Тонзиллит", "Полипы носа", "Искривление перегородки"], + }, + { + title: "Вопрос-ответ", + links: [ + "Что нужно знать до операции на ухо", + "Что нужно знать до операции на нос", + "Отзывы до и после лечения у детей", + "Что нужно знать при лечении у детей", + ], + }, + { + title: "Операции", + links: ["Септопластика", "Турбинопластика", "Тонзиллэктомия", "Аденотомия", "Тимпанопластика", "Мирингопластика"], + }, +]; + +export function FooterBlock() { + return ( +
+ {/* 4 колонки ссылок */} +
+ {FOOTER_COLUMNS.map((col) => ( +
+

+ {col.title} +

+ +
+ ))} +
+ + {/* Нижняя полоса */} +
+ {/* Логотип */} +
+
+ ✚ +
+
+
+ Клиника
ухо, горло, нос +
+
+ им. проф. Е.Н.Оленевой +
+
+
+ + {/* Адрес и соцсети */} +
+

+ Мы находимся по адресу: Пермь, ул. Г. Звезда, 31а +

+
+ {["VK", "OK", "YT", "TG"].map((s) => ( + + {s} + + ))} +
+
+ + {/* Часы работы */} +
+

+ Часы работы: +

+

Пн–пт: 9:00–21:00

+

Сб: 9:00–18:00

+

Вс: выходной

+
+
+
+ ); +} diff --git a/apps/web/components/blocks/NewsBlock.tsx b/apps/web/components/blocks/NewsBlock.tsx new file mode 100644 index 0000000..5518d7c --- /dev/null +++ b/apps/web/components/blocks/NewsBlock.tsx @@ -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 ( +
+

+ Новости +

+
+ {MOCK_NEWS.map((n) => ( +
+

+ {n.date} +

+ + {n.title} + +
+ ))} +
+
+ +
+
+ ); +} diff --git a/apps/web/components/blocks/ReviewsBlock.tsx b/apps/web/components/blocks/ReviewsBlock.tsx new file mode 100644 index 0000000..3e4eba2 --- /dev/null +++ b/apps/web/components/blocks/ReviewsBlock.tsx @@ -0,0 +1,86 @@ +const MOCK_REVIEWS = [ + { + text: "Спасибо за приём, мне всё понравилось, спасибо за приём, мне всё понравилось. Врач очень внимательный и профессиональный.", + author: "Пациент клиники", + doctor: "Тимофеева Наталья Александровна", + }, + { + text: "Очень довольна лечением! Прошла курс процедур, нос дышит отлично. Рекомендую клинику всем.", + author: "Наталья К.", + doctor: "Макарова Людмила Тимофеевна", + }, +]; + +export function ReviewsBlock() { + return ( +
+ {/* Заголовок */} +
+

+ Отзывы о нас +

+

+ За 12 лет работы наши врачи оториноларингологи избавили от болезней ухо, горло, носа + более 50 000 пациентов. Но даже сейчас мы высоко ценим каждый положительный отзыв + и искренние слова благодарности. +

+
+ + {/* Карусель */} +
+ {/* Стрелка влево */} + + + {/* Карточка отзыва */} +
+
+ « +
+

+ {MOCK_REVIEWS[0].text} +

+ + Читать отзыв полностью + +
+ + {/* Стрелка вправо */} + +
+
+ ); +} diff --git a/apps/web/components/ui/BlockMetaBar.tsx b/apps/web/components/ui/BlockMetaBar.tsx new file mode 100644 index 0000000..a80da4c --- /dev/null +++ b/apps/web/components/ui/BlockMetaBar.tsx @@ -0,0 +1,174 @@ +"use client"; + +import { useState, useEffect } from "react"; + +interface BlockMeta { + path: string; + name: string; + version: string; + isInPreview: boolean; +} + +interface BlockMetaBarProps { + path: string; + defaultVersion: string; + defaultIsInPreview: boolean; +} + +export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: BlockMetaBarProps) { + const [meta, setMeta] = useState(null); + const [editing, setEditing] = useState(false); + const [versionInput, setVersionInput] = useState(defaultVersion); + const [saving, setSaving] = useState(false); + const [togglingPreview, setTogglingPreview] = useState(false); + const [apiDown, setApiDown] = useState(false); + + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + + 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]); + + async function patch(data: { version?: string; isInPreview?: boolean }) { + 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; + } + + 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 togglePreview() { + if (!meta) return; + setTogglingPreview(true); + try { + const updated = await patch({ isInPreview: !meta.isInPreview }); + if (updated) setMeta(updated); + } finally { + setTogglingPreview(false); + } + } + + const version = meta?.version ?? defaultVersion; + const isInPreview = meta?.isInPreview ?? defaultIsInPreview; + + return ( +
+ {/* Version badge */} + + Версия: + + {editing ? ( + + setVersionInput(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter') saveVersion(); if (e.key === 'Escape') setEditing(false); }} + autoFocus + /> + + + + ) : ( + + )} + + · + + {/* Preview toggle */} + {apiDown ? ( + + + {isInPreview ? "В превью" : "Не в превью"} + + ) : ( + + )} + + {apiDown && ( + <> + · + API офлайн + + )} +
+ ); +} diff --git a/docker-compose.yml b/docker-compose.yml index 8640dda..12574d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: POSTGRES_PASSWORD: brandbook POSTGRES_DB: brandbook ports: - - "5433:5432" + - "5434:5432" volumes: - postgres_data:/var/lib/postgresql/data diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4bdf871..eece4d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,12 +19,21 @@ importers: '@nestjs/platform-express': 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) + '@prisma/adapter-pg': + specifier: ^7.5.0 + version: 7.5.0 '@prisma/client': 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) + '@types/pg': + specifier: ^8.20.0 + version: 8.20.0 dotenv: specifier: ^17.3.1 version: 17.3.1 + pg: + specifier: ^8.20.0 + version: 8.20.0 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -98,6 +107,9 @@ importers: tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 + tsx: + specifier: ^4.21.0 + version: 4.21.0 typescript: specifier: ^5.7.3 version: 5.9.3 @@ -390,6 +402,162 @@ packages: '@emnapi/wasi-threads@1.2.0': 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': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1015,6 +1183,9 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} 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': resolution: {integrity: sha512-KnJ2b4Si/pcWEtK68uM+h0h1oh80CZt2suhLTVuLaSKg4n58Q9jBF/A42Kw6Ma+aThy1yAhfDeTC0JvEmeZnFQ==} @@ -1042,6 +1213,9 @@ packages: '@prisma/dev@0.20.0': 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': resolution: {integrity: sha512-E+iRV/vbJLl8iGjVr6g/TEWokA+gjkV/doZkaQN1i/ULVdDwGnPJDfLUIFGS3BVwlG/m6L8T4x1x5isl8hGMxA==} @@ -1262,6 +1436,12 @@ packages: '@types/node@22.19.15': 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': resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} @@ -2141,6 +2321,11 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -3382,6 +3567,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} @@ -3479,6 +3667,48 @@ packages: perfect-debounce@1.0.0: 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: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3521,6 +3751,41 @@ packages: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} 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: resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} engines: {node: '>=12'} @@ -3816,6 +4081,10 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -4075,6 +4344,11 @@ packages: tslib@2.8.1: 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: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -4267,6 +4541,10 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} 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: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -4603,6 +4881,84 @@ snapshots: tslib: 2.8.1 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))': dependencies: eslint: 9.39.4(jiti@2.6.1) @@ -5282,6 +5638,15 @@ snapshots: '@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@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: - typescript + '@prisma/driver-adapter-utils@7.5.0': + dependencies: + '@prisma/debug': 7.5.0 + '@prisma/engines-version@7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e': {} '@prisma/engines@7.5.0': @@ -5554,6 +5923,18 @@ snapshots: dependencies: 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/range-parser@1.2.7': {} @@ -6530,6 +6911,35 @@ snapshots: is-date-object: 1.1.0 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: {} escape-html@1.0.3: {} @@ -8033,6 +8443,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obuf@1.1.2: {} + ohash@2.0.11: {} on-finished@2.4.1: @@ -8133,6 +8545,53 @@ snapshots: 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: {} picomatch@2.3.1: {} @@ -8169,6 +8628,28 @@ snapshots: picocolors: 1.1.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: {} prelude-ls@1.2.1: {} @@ -8532,6 +9013,8 @@ snapshots: source-map@0.7.6: {} + split2@4.2.0: {} + sprintf-js@1.0.3: {} sqlstring@2.3.3: {} @@ -8809,6 +9292,13 @@ snapshots: 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: dependencies: prelude-ls: 1.2.1 @@ -9073,6 +9563,8 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 + xtend@4.0.2: {} + y18n@5.0.8: {} yallist@3.1.1: {} diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..43a7ed3 --- /dev/null +++ b/vercel.json @@ -0,0 +1,5 @@ +{ + "buildCommand": "pnpm --filter web build", + "outputDirectory": "apps/web/.next", + "installCommand": "pnpm install" +}