From 7eb4fb731bd397fe63c167860ebbcaa961da16f9 Mon Sep 17 00:00:00 2001 From: Dokril Date: Sat, 6 Dec 2025 11:08:07 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=8C=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=20=D0=B6=D0=B5?= =?UTF-8?q?=D0=BB=D0=B0=D0=BD=D0=B8=D0=B9=20=D1=81=20=D0=B1=D1=8D=D0=BA?= =?UTF-8?q?=D0=B5=D0=BD=D0=B4=20API,=20=D0=B1=D0=B0=D0=B7=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B8=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8C?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=BC=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84?= =?UTF-8?q?=D0=B5=D0=B9=D1=81=D0=BE=D0=BC.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 26 +- admin/src/app/globals.css | 26 -- backend/.gitignore | 3 + .../migrations/0001_dashing_molten_man.sql | 29 ++ backend/migrations/meta/0001_snapshot.json | 311 +++++++++++++++ backend/migrations/meta/_journal.json | 7 + backend/package.json | 5 +- backend/seed.ts | 129 +++++++ backend/src/app.module.ts | 11 +- backend/src/database/schema.ts | 77 ++++ backend/src/wishlist/categories.controller.ts | 41 ++ backend/src/wishlist/categories.service.ts | 145 +++++++ backend/src/wishlist/dto/category.dto.ts | 16 + backend/src/wishlist/dto/wishlist-item.dto.ts | 17 + backend/src/wishlist/wishlist.controller.ts | 67 ++++ backend/src/wishlist/wishlist.module.ts | 40 ++ backend/src/wishlist/wishlist.service.ts | 159 ++++++++ backend/uploads/.gitkeep | 1 + {admin => frontend}/.gitignore | 0 {admin => frontend}/README.md | 0 {admin => frontend}/eslint.config.mjs | 0 {admin => frontend}/next.config.ts | 0 {admin => frontend}/package.json | 2 +- {admin => frontend}/pnpm-lock.yaml | 0 {admin => frontend}/postcss.config.mjs | 0 {admin => frontend}/public/file.svg | 0 {admin => frontend}/public/globe.svg | 0 {admin => frontend}/public/next.svg | 0 {admin => frontend}/public/vercel.svg | 0 {admin => frontend}/public/window.svg | 0 {admin => frontend}/src/app/favicon.ico | Bin frontend/src/app/globals.css | 146 +++++++ {admin => frontend}/src/app/layout.tsx | 0 {admin => frontend}/src/app/page.tsx | 0 frontend/src/app/wishlist/page.tsx | 35 ++ .../src/components/EventForm.tsx | 0 .../src/components/EventList.tsx | 0 .../src/components/Timeline.tsx | 0 {admin => frontend}/src/lib/api.ts | 0 {admin => frontend}/tsconfig.json | 0 pnpm-lock.yaml | 359 +++++++++++++++++- pnpm-workspace.yaml | 2 +- 42 files changed, 1610 insertions(+), 44 deletions(-) delete mode 100644 admin/src/app/globals.css create mode 100644 backend/.gitignore create mode 100644 backend/migrations/0001_dashing_molten_man.sql create mode 100644 backend/migrations/meta/0001_snapshot.json create mode 100644 backend/seed.ts create mode 100644 backend/src/wishlist/categories.controller.ts create mode 100644 backend/src/wishlist/categories.service.ts create mode 100644 backend/src/wishlist/dto/category.dto.ts create mode 100644 backend/src/wishlist/dto/wishlist-item.dto.ts create mode 100644 backend/src/wishlist/wishlist.controller.ts create mode 100644 backend/src/wishlist/wishlist.module.ts create mode 100644 backend/src/wishlist/wishlist.service.ts create mode 100644 backend/uploads/.gitkeep rename {admin => frontend}/.gitignore (100%) rename {admin => frontend}/README.md (100%) rename {admin => frontend}/eslint.config.mjs (100%) rename {admin => frontend}/next.config.ts (100%) rename {admin => frontend}/package.json (93%) rename {admin => frontend}/pnpm-lock.yaml (100%) rename {admin => frontend}/postcss.config.mjs (100%) rename {admin => frontend}/public/file.svg (100%) rename {admin => frontend}/public/globe.svg (100%) rename {admin => frontend}/public/next.svg (100%) rename {admin => frontend}/public/vercel.svg (100%) rename {admin => frontend}/public/window.svg (100%) rename {admin => frontend}/src/app/favicon.ico (100%) create mode 100644 frontend/src/app/globals.css rename {admin => frontend}/src/app/layout.tsx (100%) rename {admin => frontend}/src/app/page.tsx (100%) create mode 100644 frontend/src/app/wishlist/page.tsx rename {admin => frontend}/src/components/EventForm.tsx (100%) rename {admin => frontend}/src/components/EventList.tsx (100%) rename {admin => frontend}/src/components/Timeline.tsx (100%) rename {admin => frontend}/src/lib/api.ts (100%) rename {admin => frontend}/tsconfig.json (100%) diff --git a/README.md b/README.md index 3063861..c6803b8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # home-service -Современный монорепозиторий для сервиса домашней автоматизации с бэкендом и админ-панелью. +Современный монорепозиторий для сервиса домашней автоматизации с бэкендом и фронтенд-панелью. ## 🚀 Стек технологий @@ -15,7 +15,7 @@ - Zod для валидации - date-fns для работы с датами -**Admin:** +**Frontend:** - Next.js 16 (App Router) - React 19 - TailwindCSS 4 @@ -34,7 +34,7 @@ home-service/ │ ├── migrations/ # Миграции БД │ └── data/ # База данных PGLite │ -├── admin/ # Next.js админка (@home-service/admin) +├── frontend/ # Next.js админка (@home-service/frontend) │ └── src/ │ ├── app/ # Страницы │ ├── components/ # React компоненты @@ -86,13 +86,13 @@ pnpm dev # Запустить только backend pnpm --filter @home-service/backend dev -# Запустить только admin -pnpm --filter @home-service/admin dev +# Запустить только frontend +pnpm --filter @home-service/frontend dev ``` **Адреса:** - Backend API: `http://localhost:3000` -- Admin панель: `http://localhost:3001` +- Frontend панель: `http://localhost:3001` ### Сборка @@ -103,8 +103,8 @@ pnpm build # Собрать только backend pnpm --filter @home-service/backend build -# Собрать только admin -pnpm --filter @home-service/admin build +# Собрать только frontend +pnpm --filter @home-service/frontend build ``` ### Другие команды @@ -183,7 +183,7 @@ CORS_ORIGIN=http://localhost:3001 DATABASE_PATH=./data/events.db ``` -### Admin (.env.local) +### Frontend (.env.local) ``` NEXT_PUBLIC_API_URL=http://localhost:3000 ``` @@ -205,7 +205,7 @@ curl -X POST http://localhost:3000/api/events \ ```bash pnpm build pnpm --filter @home-service/backend start:prod -pnpm --filter @home-service/admin start +pnpm --filter @home-service/frontend start ``` ### Backend отдельно @@ -214,10 +214,10 @@ pnpm --filter @home-service/backend build pnpm --filter @home-service/backend start:prod ``` -### Admin отдельно +### Frontend отдельно ```bash -pnpm --filter @home-service/admin build -pnpm --filter @home-service/admin start +pnpm --filter @home-service/frontend build +pnpm --filter @home-service/frontend start ``` ## 🛠️ Разработка diff --git a/admin/src/app/globals.css b/admin/src/app/globals.css deleted file mode 100644 index a2dc41e..0000000 --- a/admin/src/app/globals.css +++ /dev/null @@ -1,26 +0,0 @@ -@import "tailwindcss"; - -:root { - --background: #ffffff; - --foreground: #171717; -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..91eaf49 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,3 @@ + +uploads/* +!uploads/.gitkeep diff --git a/backend/migrations/0001_dashing_molten_man.sql b/backend/migrations/0001_dashing_molten_man.sql new file mode 100644 index 0000000..0871435 --- /dev/null +++ b/backend/migrations/0001_dashing_molten_man.sql @@ -0,0 +1,29 @@ +CREATE TABLE "wishlist_categories" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "slug" text NOT NULL, + "min_price" integer DEFAULT 0 NOT NULL, + "max_price" integer, + "color" text, + "icon" text, + "order" integer DEFAULT 0 NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "wishlist_categories_name_unique" UNIQUE("name"), + CONSTRAINT "wishlist_categories_slug_unique" UNIQUE("slug") +); +--> statement-breakpoint +CREATE TABLE "wishlist_items" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "title" text NOT NULL, + "description" text, + "price" integer NOT NULL, + "currency" text DEFAULT 'RUB' NOT NULL, + "link" text, + "images" text[] DEFAULT ARRAY[]::text[] NOT NULL, + "category_id" uuid NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "wishlist_items" ADD CONSTRAINT "wishlist_items_category_id_wishlist_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."wishlist_categories"("id") ON DELETE restrict ON UPDATE no action; \ No newline at end of file diff --git a/backend/migrations/meta/0001_snapshot.json b/backend/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..6ed17d5 --- /dev/null +++ b/backend/migrations/meta/0001_snapshot.json @@ -0,0 +1,311 @@ +{ + "id": "172d78f6-8d17-4bc0-882d-c85b984f0c22", + "prevId": "852b1a6a-31c7-4f44-bdef-a378a18e2c7d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "month": { + "name": "month", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "day": { + "name": "day", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "start_year": { + "name": "start_year", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "end_month": { + "name": "end_month", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "end_day": { + "name": "end_day", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "end_year": { + "name": "end_year", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wishlist_categories": { + "name": "wishlist_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "min_price": { + "name": "min_price", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_price": { + "name": "max_price", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "wishlist_categories_name_unique": { + "name": "wishlist_categories_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + }, + "wishlist_categories_slug_unique": { + "name": "wishlist_categories_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wishlist_items": { + "name": "wishlist_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price": { + "name": "price", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'RUB'" + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "images": { + "name": "images", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "category_id": { + "name": "category_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "wishlist_items_category_id_wishlist_categories_id_fk": { + "name": "wishlist_items_category_id_wishlist_categories_id_fk", + "tableFrom": "wishlist_items", + "tableTo": "wishlist_categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/backend/migrations/meta/_journal.json b/backend/migrations/meta/_journal.json index b989f10..d8c1ff9 100644 --- a/backend/migrations/meta/_journal.json +++ b/backend/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1764421778123, "tag": "0000_lyrical_gabe_jones", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1765007898910, + "tag": "0001_dashing_molten_man", + "breakpoints": true } ] } \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 8518030..844763b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -25,7 +25,8 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", - "@nestjs/platform-express": "^11.0.1", + "@nestjs/platform-express": "^11.1.9", + "@nestjs/serve-static": "^5.0.4", "class-transformer": "^0.5.1", "class-validator": "^0.14.3", "date-fns": "^4.1.0", @@ -41,6 +42,7 @@ "@nestjs/testing": "^11.0.1", "@types/express": "^5.0.0", "@types/jest": "^30.0.0", + "@types/multer": "^2.0.0", "@types/node": "^22.19.1", "@types/supertest": "^6.0.2", "drizzle-kit": "^0.31.7", @@ -51,6 +53,7 @@ "ts-loader": "^9.5.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", + "tsx": "^4.21.0", "typescript": "^5.7.3" }, "jest": { diff --git a/backend/seed.ts b/backend/seed.ts new file mode 100644 index 0000000..140f2f2 --- /dev/null +++ b/backend/seed.ts @@ -0,0 +1,129 @@ +import { DatabaseService } from './src/database/database.service'; +import { wishlistCategories, wishlistItems } from './src/database/schema'; + +async function seed() { + const db = new DatabaseService(); + + console.log('🌱 Seeding wishlist categories...'); + + // Создать категории + const categories = await db.database + .insert(wishlistCategories) + .values([ + { + name: 'БЮДЖЕТНО', + slug: 'tier-1', + minPrice: 0, + maxPrice: 150000, // 1500 руб + color: '#00ff41', + icon: '🟢', + order: 1, + }, + { + name: 'СРЕДНИЙ', + slug: 'tier-2', + minPrice: 150001, + maxPrice: 500000, // 5000 руб + color: '#00cc33', + icon: '🟡', + order: 2, + }, + { + name: 'ТОП', + slug: 'tier-3', + minPrice: 500001, + maxPrice: null, // без ограничения + color: '#009922', + icon: '🔴', + order: 3, + }, + ]) + .returning(); + + console.log(`✅ Created ${categories.length} categories`); + + console.log('🌱 Seeding wishlist items...'); + + // Создать примерные товары + const items = await db.database + .insert(wishlistItems) + .values([ + { + title: 'Зерновой Кофе (Эфиопия)', + description: 'Люблю светлую обжарку. Желательно Эфиопия или Кения. Нужен именно в зернах.', + price: 85000, // 850 руб + currency: 'RUB', + link: 'https://ozon.ru', + images: [ + 'url:https://images.unsplash.com/photo-1497935586351-b67a49e012bf?w=500', + ], + categoryId: categories[0].id, // БЮДЖЕТНО + }, + { + title: 'Молескин в точку', + description: 'Черный, классический. Обязательно в точку, а не в линейку.', + price: 120000, // 1200 руб + currency: 'RUB', + link: 'https://wildberries.ru', + images: [ + 'url:https://images.unsplash.com/photo-1531346878377-a513bc957374?w=500', + ], + categoryId: categories[0].id, // БЮДЖЕТНО + }, + { + title: 'Винил: Daft Punk', + description: 'Альбом "Random Access Memories". Мечтаю послушать его на проигрывателе.', + price: 350000, // 3500 руб + currency: 'RUB', + link: 'https://market.yandex.ru', + images: [ + 'url:https://images.unsplash.com/photo-1603048588665-791ca8aea617?w=500', + ], + categoryId: categories[1].id, // СРЕДНИЙ + }, + { + title: 'D&D Стартовый набор', + description: '5-я редакция. Хочу попробовать поиграть с друзьями.', + price: 290000, // 2900 руб + currency: 'RUB', + link: 'https://hobbygames.ru', + images: [ + 'url:https://images.unsplash.com/photo-1632501641765-e568d90e09b2?w=500', + ], + categoryId: categories[1].id, // СРЕДНИЙ + }, + { + title: 'LEGO Speed Champions', + description: 'Любая машинка из этой серии, желательно Porsche или Ferrari.', + price: 250000, // 2500 руб + currency: 'RUB', + link: 'https://detmir.ru', + images: [ + 'url:https://images.unsplash.com/photo-1585366119957-e9730b6d0f60?w=500', + ], + categoryId: categories[1].id, // СРЕДНИЙ + }, + { + title: 'Keychron K2', + description: 'Механическая клавиатура. Свичи Red или Brown. Нужна подсветка.', + price: 900000, // 9000 руб + currency: 'RUB', + link: 'https://geekboards.ru', + images: [ + 'url:https://images.unsplash.com/photo-1595225476474-87563907a212?w=500', + ], + categoryId: categories[2].id, // ТОП + }, + ]) + .returning(); + + console.log(`✅ Created ${items.length} wishlist items`); + console.log('✨ Seeding completed!'); + + process.exit(0); +} + +seed().catch((error) => { + console.error('❌ Seeding failed:', error); + process.exit(1); +}); diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index baddba4..b852977 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,15 +1,24 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { ServeStaticModule } from '@nestjs/serve-static'; +import { join } from 'path'; import { DatabaseModule } from './database/database.module'; import { EventsModule } from './events/events.module'; +import { WishlistModule } from './wishlist/wishlist.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, }), + ServeStaticModule.forRoot({ + rootPath: join(__dirname, '..', 'uploads'), + serveRoot: '/uploads', + }), DatabaseModule, EventsModule, + WishlistModule, ], }) -export class AppModule {} +export class AppModule { } + diff --git a/backend/src/database/schema.ts b/backend/src/database/schema.ts index 0cc9b47..c77d207 100644 --- a/backend/src/database/schema.ts +++ b/backend/src/database/schema.ts @@ -37,3 +37,80 @@ export const events = pgTable('events', { export type Event = typeof events.$inferSelect; export type NewEvent = typeof events.$inferInsert; + +// Wishlist Categories Table +export const wishlistCategories = pgTable('wishlist_categories', { + id: uuid('id') + .primaryKey() + .default(sql`gen_random_uuid()`), + + // Основная информация + name: text('name').notNull().unique(), // "БЮДЖЕТНО", "СРЕДНИЙ", "ТОП" + slug: text('slug').notNull().unique(), // "tier-1", "tier-2", "tier-3" + + // Диапазон цен (в копейках) + minPrice: integer('min_price').notNull().default(0), + maxPrice: integer('max_price'), // null = без ограничения + + // Визуальные настройки + color: text('color'), // hex цвет для UI + icon: text('icon'), // emoji или класс иконки + order: integer('order').notNull().default(0), // порядок сортировки + + // Служебные поля + createdAt: timestamp('created_at', { withTimezone: true }) + .notNull() + .defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }) + .notNull() + .defaultNow() + .$onUpdate(() => new Date()), +}); + +export type WishlistCategory = typeof wishlistCategories.$inferSelect; +export type NewWishlistCategory = typeof wishlistCategories.$inferInsert; + +// Wishlist Items Table +export const wishlistItems = pgTable('wishlist_items', { + id: uuid('id') + .primaryKey() + .default(sql`gen_random_uuid()`), + + // Основная информация + title: text('title').notNull(), + description: text('description'), + price: integer('price').notNull(), // в копейках + currency: text('currency').notNull().default('RUB'), + + // Ссылка на товар + link: text('link'), + + // Множественные изображения (массив строк) + // Формат: ["url:https://...", "uploaded:/uploads/wishlist/file.jpg", ...] + images: text('images').array().notNull().default(sql`ARRAY[]::text[]`), + + // Связь с категорией + categoryId: uuid('category_id') + .notNull() + .references(() => wishlistCategories.id, { onDelete: 'restrict' }), + + // Служебные поля + createdAt: timestamp('created_at', { withTimezone: true }) + .notNull() + .defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }) + .notNull() + .defaultNow() + .$onUpdate(() => new Date()), +}); + +export type WishlistItem = typeof wishlistItems.$inferSelect; +export type NewWishlistItem = typeof wishlistItems.$inferInsert; + +// Helper type for image objects +export type WishlistImage = { + type: 'url' | 'uploaded'; + path: string; + alt?: string; +}; + diff --git a/backend/src/wishlist/categories.controller.ts b/backend/src/wishlist/categories.controller.ts new file mode 100644 index 0000000..38d5861 --- /dev/null +++ b/backend/src/wishlist/categories.controller.ts @@ -0,0 +1,41 @@ +import { Controller, Get, Post, Patch, Delete, Body, Param, UsePipes } from '@nestjs/common'; +import { WishlistCategoriesService } from './categories.service'; +import { CreateWishlistCategoryDto, UpdateWishlistCategoryDto } from './dto/category.dto'; +import { ZodValidationPipe } from '../pipes/zod-validation.pipe'; +import { CreateWishlistCategorySchema, UpdateWishlistCategorySchema } from './dto/category.dto'; + +@Controller('api/wishlist/categories') +export class WishlistCategoriesController { + constructor(private readonly categoriesService: WishlistCategoriesService) { } + + @Get() + async findAll() { + return this.categoriesService.findAll(); + } + + @Get(':id') + async findOne(@Param('id') id: string) { + return this.categoriesService.findOne(id); + } + + @Post() + @UsePipes(new ZodValidationPipe(CreateWishlistCategorySchema)) + async create(@Body() dto: CreateWishlistCategoryDto) { + return this.categoriesService.create(dto); + } + + @Patch(':id') + @UsePipes(new ZodValidationPipe(UpdateWishlistCategorySchema)) + async update( + @Param('id') id: string, + @Body() dto: UpdateWishlistCategoryDto + ) { + return this.categoriesService.update(id, dto); + } + + @Delete(':id') + async delete(@Param('id') id: string) { + await this.categoriesService.delete(id); + return { message: 'Category deleted successfully' }; + } +} diff --git a/backend/src/wishlist/categories.service.ts b/backend/src/wishlist/categories.service.ts new file mode 100644 index 0000000..9bf842a --- /dev/null +++ b/backend/src/wishlist/categories.service.ts @@ -0,0 +1,145 @@ +import { Injectable, ConflictException, NotFoundException } from '@nestjs/common'; +import { eq, and, lte, gte, or, isNull, asc } from 'drizzle-orm'; +import { wishlistCategories, WishlistCategory, NewWishlistCategory } from '../database/schema'; +import { CreateWishlistCategoryDto, UpdateWishlistCategoryDto } from './dto/category.dto'; + +@Injectable() +export class WishlistCategoriesService { + constructor(private readonly db: DatabaseService) { } + + async findAll(): Promise { + return this.db.database + .select() + .from(wishlistCategories) + .orderBy(asc(wishlistCategories.order)); + } + + async findOne(id: string): Promise { + const [category] = await this.db.database + .select() + .from(wishlistCategories) + .where(eq(wishlistCategories.id, id)) + .limit(1); + + if (!category) { + throw new NotFoundException(`Category with ID ${id} not found`); + } + + return category; + } + + async findBySlug(slug: string): Promise { + const [category] = await this.db.database + .select() + .from(wishlistCategories) + .where(eq(wishlistCategories.slug, slug)) + .limit(1); + + return category || null; + } + + async create(dto: CreateWishlistCategoryDto): Promise { + // Проверить уникальность slug + const existing = await this.findBySlug(dto.slug); + if (existing) { + throw new ConflictException(`Category with slug "${dto.slug}" already exists`); + } + + // Проверить пересечение диапазонов цен + await this.validatePriceRange(dto.minPrice, dto.maxPrice); + + const newCategory: NewWishlistCategory = { + ...dto, + createdAt: new Date(), + updatedAt: new Date(), + }; + + const [category] = await this.db.database + .insert(wishlistCategories) + .values(newCategory) + .returning(); + + return category; + } + + async update(id: string, dto: UpdateWishlistCategoryDto): Promise { + const category = await this.findOne(id); + + // Проверить уникальность slug если изменяется + if (dto.slug && dto.slug !== category.slug) { + const existing = await this.findBySlug(dto.slug); + if (existing) { + throw new ConflictException(`Category with slug "${dto.slug}" already exists`); + } + } + + // Проверить пересечение диапазонов цен если изменяются + if (dto.minPrice !== undefined || dto.maxPrice !== undefined) { + const minPrice = dto.minPrice ?? category.minPrice; + const maxPrice = dto.maxPrice ?? category.maxPrice; + await this.validatePriceRange(minPrice, maxPrice, id); + } + + const [updated] = await this.db.database + .update(wishlistCategories) + .set({ ...dto, updatedAt: new Date() }) + .where(eq(wishlistCategories.id, id)) + .returning(); + + return updated; + } + + async delete(id: string): Promise { + await this.findOne(id); + + await this.db.database + .delete(wishlistCategories) + .where(eq(wishlistCategories.id, id)); + } + + async findCategoryByPrice(price: number): Promise { + const [category] = await this.db.database + .select() + .from(wishlistCategories) + .where( + and( + lte(wishlistCategories.minPrice, price), + or( + gte(wishlistCategories.maxPrice, price), + isNull(wishlistCategories.maxPrice) + ) + ) + ) + .orderBy(asc(wishlistCategories.order)) + .limit(1); + + return category || null; + } + + private async validatePriceRange( + minPrice: number, + maxPrice: number | null | undefined, + excludeId?: string + ): Promise { + const categories = await this.findAll(); + + for (const cat of categories) { + if (excludeId && cat.id === excludeId) continue; + + const catMin = cat.minPrice; + const catMax = cat.maxPrice; + + // Проверить пересечение диапазонов + const hasOverlap = + (maxPrice === null || maxPrice === undefined || catMax === null || catMax === undefined) + ? true // Если хотя бы один диапазон открыт сверху + : (minPrice <= catMax && maxPrice >= catMin); + + if (hasOverlap && !(maxPrice !== null && maxPrice !== undefined && maxPrice < catMin)) { + throw new ConflictException( + `Price range [${minPrice}, ${maxPrice || '∞'}] overlaps with category "${cat.name}" [${catMin}, ${catMax || '∞'}]` + ); + } + } + } +} diff --git a/backend/src/wishlist/dto/category.dto.ts b/backend/src/wishlist/dto/category.dto.ts new file mode 100644 index 0000000..2406f9e --- /dev/null +++ b/backend/src/wishlist/dto/category.dto.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +export const CreateWishlistCategorySchema = z.object({ + name: z.string().min(1).max(100), + slug: z.string().regex(/^[a-z0-9-]+$/), + minPrice: z.number().int().min(0).default(0), + maxPrice: z.number().int().positive().optional(), + color: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(), + icon: z.string().optional(), + order: z.number().int().default(0), +}); + +export const UpdateWishlistCategorySchema = CreateWishlistCategorySchema.partial(); + +export type CreateWishlistCategoryDto = z.infer; +export type UpdateWishlistCategoryDto = z.infer; diff --git a/backend/src/wishlist/dto/wishlist-item.dto.ts b/backend/src/wishlist/dto/wishlist-item.dto.ts new file mode 100644 index 0000000..88ae5ff --- /dev/null +++ b/backend/src/wishlist/dto/wishlist-item.dto.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; + +export const CreateWishlistItemSchema = z.object({ + title: z.string().min(1).max(200), + description: z.string().max(1000).optional(), + price: z.number().int().positive(), + currency: z.string().default('RUB'), + link: z.string().url().optional().or(z.literal('')), + // Images будут обрабатываться отдельно (файлы + URL) + imageUrls: z.array(z.string().url()).optional().default([]), + categoryId: z.string().uuid(), +}); + +export const UpdateWishlistItemSchema = CreateWishlistItemSchema.partial(); + +export type CreateWishlistItemDto = z.infer; +export type UpdateWishlistItemDto = z.infer; diff --git a/backend/src/wishlist/wishlist.controller.ts b/backend/src/wishlist/wishlist.controller.ts new file mode 100644 index 0000000..7390ed0 --- /dev/null +++ b/backend/src/wishlist/wishlist.controller.ts @@ -0,0 +1,67 @@ +import { + Controller, + Get, + Post, + Patch, + Delete, + Body, + Param, + Query, + UseInterceptors, + UploadedFiles, + UsePipes, +} from '@nestjs/common'; +import { FilesInterceptor } from '@nestjs/platform-express'; +import { WishlistService } from './wishlist.service'; +import { type CreateWishlistItemDto, type UpdateWishlistItemDto } from './dto/wishlist-item.dto'; +import { ZodValidationPipe } from '../pipes/zod-validation.pipe'; +import { CreateWishlistItemSchema, UpdateWishlistItemSchema } from './dto/wishlist-item.dto'; + +@Controller('api/wishlist') +export class WishlistController { + constructor(private readonly wishlistService: WishlistService) { } + + @Get() + async findAll(@Query('categoryId') categoryId?: string) { + return this.wishlistService.findAll(categoryId); + } + + @Get(':id') + async findOne(@Param('id') id: string) { + return this.wishlistService.findOne(id); + } + + @Post() + @UseInterceptors(FilesInterceptor('images', 5)) + @UsePipes(new ZodValidationPipe(CreateWishlistItemSchema)) + async create( + @Body() dto: CreateWishlistItemDto, + @UploadedFiles() files?: Express.Multer.File[] + ) { + return this.wishlistService.create(dto, files); + } + + @Patch(':id') + @UseInterceptors(FilesInterceptor('images', 5)) + @UsePipes(new ZodValidationPipe(UpdateWishlistItemSchema)) + async update( + @Param('id') id: string, + @Body() dto: UpdateWishlistItemDto, + @UploadedFiles() files?: Express.Multer.File[] + ) { + return this.wishlistService.update(id, dto, files); + } + + @Delete(':id') + async delete(@Param('id') id: string) { + await this.wishlistService.delete(id); + return { message: 'Wishlist item deleted successfully' }; + } + + @Post('upload') + @UseInterceptors(FilesInterceptor('images', 5)) + async uploadImages(@UploadedFiles() files: Express.Multer.File[]) { + const paths = await this.wishlistService.uploadImages(files); + return { images: paths }; + } +} diff --git a/backend/src/wishlist/wishlist.module.ts b/backend/src/wishlist/wishlist.module.ts new file mode 100644 index 0000000..684ae62 --- /dev/null +++ b/backend/src/wishlist/wishlist.module.ts @@ -0,0 +1,40 @@ +import { Module } from '@nestjs/common'; +import { MulterModule } from '@nestjs/platform-express'; +import { diskStorage } from 'multer'; +import { extname } from 'path'; +import { WishlistController } from './wishlist.controller'; +import { WishlistCategoriesController } from './categories.controller'; +import { WishlistService } from './wishlist.service'; +import { WishlistCategoriesService } from './categories.service'; +import { DatabaseModule } from '../database/database.module'; + +@Module({ + imports: [ + DatabaseModule, + MulterModule.register({ + storage: diskStorage({ + destination: './uploads/wishlist', + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); + const ext = extname(file.originalname); + cb(null, `${uniqueSuffix}${ext}`); + }, + }), + limits: { + fileSize: 5 * 1024 * 1024, // 5MB + }, + fileFilter: (req, file, cb) => { + // Разрешить только изображения + if (file.mimetype.match(/^image\//)) { + cb(null, true); + } else { + cb(new Error('Only image files are allowed'), false); + } + }, + }), + ], + controllers: [WishlistController, WishlistCategoriesController], + providers: [WishlistService, WishlistCategoriesService], + exports: [WishlistService, WishlistCategoriesService], +}) +export class WishlistModule { } diff --git a/backend/src/wishlist/wishlist.service.ts b/backend/src/wishlist/wishlist.service.ts new file mode 100644 index 0000000..59e0645 --- /dev/null +++ b/backend/src/wishlist/wishlist.service.ts @@ -0,0 +1,159 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { eq } from 'drizzle-orm'; +import { DatabaseService } from '../database/database.service'; +import { wishlistItems, WishlistItem, NewWishlistItem } from '../database/schema'; +import { CreateWishlistItemDto, UpdateWishlistItemDto } from './dto/wishlist-item.dto'; +import { WishlistCategoriesService } from './categories.service'; +import * as fs from 'fs/promises'; +import * as path from 'path'; + +@Injectable() +export class WishlistService { + constructor( + private readonly db: DatabaseService, + private readonly categoriesService: WishlistCategoriesService, + ) { } + + async findAll(categoryId?: string): Promise { + const query = this.db.database.select().from(wishlistItems); + + if (categoryId) { + query.where(eq(wishlistItems.categoryId, categoryId)); + } + + return query; + } + + async findOne(id: string): Promise { + const [item] = await this.db.database + .select() + .from(wishlistItems) + .where(eq(wishlistItems.id, id)) + .limit(1); + + if (!item) { + throw new NotFoundException(`Wishlist item with ID ${id} not found`); + } + + return item; + } + + async create( + dto: CreateWishlistItemDto, + uploadedFiles?: Express.Multer.File[] + ): Promise { + // Проверить существование категории + await this.categoriesService.findOne(dto.categoryId); + + // Собрать массив изображений + const images: string[] = []; + + // Добавить загруженные файлы + if (uploadedFiles && uploadedFiles.length > 0) { + for (const file of uploadedFiles) { + images.push(`uploaded:/uploads/wishlist/${file.filename}`); + } + } + + // Добавить URL изображения + if (dto.imageUrls && dto.imageUrls.length > 0) { + for (const url of dto.imageUrls) { + images.push(`url:${url}`); + } + } + + const newItem: NewWishlistItem = { + title: dto.title, + description: dto.description, + price: dto.price, + currency: dto.currency, + link: dto.link, + images, + categoryId: dto.categoryId, + createdAt: new Date(), + updatedAt: new Date(), + }; + + const [item] = await this.db.database + .insert(wishlistItems) + .values(newItem) + .returning(); + + return item; + } + + async update( + id: string, + dto: UpdateWishlistItemDto, + uploadedFiles?: Express.Multer.File[] + ): Promise { + const item = await this.findOne(id); + + // Проверить существование категории если изменяется + if (dto.categoryId) { + await this.categoriesService.findOne(dto.categoryId); + } + + // Обновить массив изображений + let images = [...item.images]; + + // Добавить новые загруженные файлы + if (uploadedFiles && uploadedFiles.length > 0) { + for (const file of uploadedFiles) { + images.push(`uploaded:/uploads/wishlist/${file.filename}`); + } + } + + // Добавить новые URL + if (dto.imageUrls && dto.imageUrls.length > 0) { + for (const url of dto.imageUrls) { + if (!images.includes(`url:${url}`)) { + images.push(`url:${url}`); + } + } + } + + const [updated] = await this.db.database + .update(wishlistItems) + .set({ + ...dto, + images, + updatedAt: new Date(), + }) + .where(eq(wishlistItems.id, id)) + .returning(); + + return updated; + } + + async delete(id: string): Promise { + const item = await this.findOne(id); + + // Удалить загруженные файлы + await this.deleteUploadedImages(item.images); + + await this.db.database + .delete(wishlistItems) + .where(eq(wishlistItems.id, id)); + } + + async uploadImages(files: Express.Multer.File[]): Promise { + return files.map(file => `/uploads/wishlist/${file.filename}`); + } + + private async deleteUploadedImages(images: string[]): Promise { + const uploadedImages = images.filter(img => img.startsWith('uploaded:')); + + for (const img of uploadedImages) { + const filePath = img.replace('uploaded:', ''); + const fullPath = path.join(process.cwd(), filePath); + + try { + await fs.unlink(fullPath); + } catch (error) { + // Игнорировать ошибки удаления (файл может не существовать) + console.warn(`Failed to delete file ${fullPath}:`, error); + } + } + } +} diff --git a/backend/uploads/.gitkeep b/backend/uploads/.gitkeep new file mode 100644 index 0000000..211f88a --- /dev/null +++ b/backend/uploads/.gitkeep @@ -0,0 +1 @@ +# Keep this directory in git but ignore uploaded files diff --git a/admin/.gitignore b/frontend/.gitignore similarity index 100% rename from admin/.gitignore rename to frontend/.gitignore diff --git a/admin/README.md b/frontend/README.md similarity index 100% rename from admin/README.md rename to frontend/README.md diff --git a/admin/eslint.config.mjs b/frontend/eslint.config.mjs similarity index 100% rename from admin/eslint.config.mjs rename to frontend/eslint.config.mjs diff --git a/admin/next.config.ts b/frontend/next.config.ts similarity index 100% rename from admin/next.config.ts rename to frontend/next.config.ts diff --git a/admin/package.json b/frontend/package.json similarity index 93% rename from admin/package.json rename to frontend/package.json index a929cc1..9a87702 100644 --- a/admin/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "@home-service/admin", + "name": "@home-service/frontend", "version": "0.1.0", "private": true, "scripts": { diff --git a/admin/pnpm-lock.yaml b/frontend/pnpm-lock.yaml similarity index 100% rename from admin/pnpm-lock.yaml rename to frontend/pnpm-lock.yaml diff --git a/admin/postcss.config.mjs b/frontend/postcss.config.mjs similarity index 100% rename from admin/postcss.config.mjs rename to frontend/postcss.config.mjs diff --git a/admin/public/file.svg b/frontend/public/file.svg similarity index 100% rename from admin/public/file.svg rename to frontend/public/file.svg diff --git a/admin/public/globe.svg b/frontend/public/globe.svg similarity index 100% rename from admin/public/globe.svg rename to frontend/public/globe.svg diff --git a/admin/public/next.svg b/frontend/public/next.svg similarity index 100% rename from admin/public/next.svg rename to frontend/public/next.svg diff --git a/admin/public/vercel.svg b/frontend/public/vercel.svg similarity index 100% rename from admin/public/vercel.svg rename to frontend/public/vercel.svg diff --git a/admin/public/window.svg b/frontend/public/window.svg similarity index 100% rename from admin/public/window.svg rename to frontend/public/window.svg diff --git a/admin/src/app/favicon.ico b/frontend/src/app/favicon.ico similarity index 100% rename from admin/src/app/favicon.ico rename to frontend/src/app/favicon.ico diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 0000000..6bf5363 --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,146 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; + + /* Terminal theme colors */ + --term-bg: #0a0a0a; + --term-green: #00ff41; + --term-dim: #008f11; + --term-dark: #0d0208; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + + /* Terminal theme utilities */ + --color-term-bg: var(--term-bg); + --color-term-green: var(--term-green); + --color-term-dim: var(--term-dim); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} + +/* Terminal theme classes */ +.bg-term-bg { + background-color: var(--term-bg); +} + +.text-term-green { + color: var(--term-green); +} + +.text-term-dim { + color: var(--term-dim); +} + +/* Custom Scrollbar */ +::-webkit-scrollbar { + width: 10px; +} + +::-webkit-scrollbar-track { + background: var(--term-bg); + border-left: 1px solid var(--term-dim); +} + +::-webkit-scrollbar-thumb { + background: var(--term-dim); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--term-green); +} + +/* Scanline effect */ +.scanline { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: linear-gradient( + to bottom, + rgba(255, 255, 255, 0), + rgba(255, 255, 255, 0) 50%, + rgba(0, 0, 0, 0.1) 50%, + rgba(0, 0, 0, 0.1) + ); + background-size: 100% 4px; + pointer-events: none; + z-index: 50; +} + +/* Terminal box */ +.term-border { + border: 1px solid var(--term-dim); +} + +.term-box { + border: 1px solid var(--term-dim); + box-shadow: 2px 2px 0px var(--term-dim); + transition: all 0.2s ease; +} + +.term-box:hover { + box-shadow: 4px 4px 0px var(--term-green); + border-color: var(--term-green); + transform: translate(-2px, -2px); +} + +/* Image effects */ +.term-img { + filter: grayscale(100%) contrast(1.2) brightness(0.8); + transition: filter 0.3s; +} + +.term-box:hover .term-img { + filter: grayscale(0%) contrast(1) brightness(1); +} + +/* Cursor blinkAnimation */ +@keyframes blink { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0; + } +} + +.cursor-blink { + animation: blink 1s step-end infinite; +} + +/* Nav link hover */ +.nav-link { + transition: all 0.2s; +} + +.nav-link:hover { + background-color: var(--term-green); + color: #000; +} + +.active-link { + background-color: var(--term-green); + color: #000; + font-weight: bold; +} + diff --git a/admin/src/app/layout.tsx b/frontend/src/app/layout.tsx similarity index 100% rename from admin/src/app/layout.tsx rename to frontend/src/app/layout.tsx diff --git a/admin/src/app/page.tsx b/frontend/src/app/page.tsx similarity index 100% rename from admin/src/app/page.tsx rename to frontend/src/app/page.tsx diff --git a/frontend/src/app/wishlist/page.tsx b/frontend/src/app/wishlist/page.tsx new file mode 100644 index 0000000..c8e120f --- /dev/null +++ b/frontend/src/app/wishlist/page.tsx @@ -0,0 +1,35 @@ +export default function WishlistPage() { + return ( +
+
+
+
+
+
+

+ > WISHLIST_LOADED_ +

+

+ // USER: ALEX
+ // STATUS: WAITING_FOR_DROPS
+ // LOCATION: EARTH_C-137
+
+ Привет. Это мой список желаний. Я люблю технику, книги и старый винил. + Ниже список вещей, которые сделают меня немного счастливее. + Выбирайте любой уровень сложности (цены). +

+
+
+
2025
+
+
+
+ +
+

Frontend реализация в процессе...

+

Backend API готов на http://localhost:3000/api/wishlist

+
+
+
+ ); +} diff --git a/admin/src/components/EventForm.tsx b/frontend/src/components/EventForm.tsx similarity index 100% rename from admin/src/components/EventForm.tsx rename to frontend/src/components/EventForm.tsx diff --git a/admin/src/components/EventList.tsx b/frontend/src/components/EventList.tsx similarity index 100% rename from admin/src/components/EventList.tsx rename to frontend/src/components/EventList.tsx diff --git a/admin/src/components/Timeline.tsx b/frontend/src/components/Timeline.tsx similarity index 100% rename from admin/src/components/Timeline.tsx rename to frontend/src/components/Timeline.tsx diff --git a/admin/src/lib/api.ts b/frontend/src/lib/api.ts similarity index 100% rename from admin/src/lib/api.ts rename to frontend/src/lib/api.ts diff --git a/admin/tsconfig.json b/frontend/tsconfig.json similarity index 100% rename from admin/tsconfig.json rename to frontend/tsconfig.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea01887..f722488 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,8 +64,11 @@ importers: specifier: ^11.0.1 version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': - specifier: ^11.0.1 + specifier: ^11.1.9 version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + '@nestjs/serve-static': + specifier: ^5.0.4 + version: 5.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(express@5.1.0) class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -106,6 +109,9 @@ importers: '@types/jest': specifier: ^30.0.0 version: 30.0.0 + '@types/multer': + specifier: ^2.0.0 + version: 2.0.0 '@types/node': specifier: ^22.19.1 version: 22.19.1 @@ -136,10 +142,50 @@ 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 + frontend: + dependencies: + next: + specifier: 16.0.5 + version: 16.0.5(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: + specifier: 19.2.0 + version: 19.2.0 + react-dom: + specifier: 19.2.0 + version: 19.2.0(react@19.2.0) + devDependencies: + '@tailwindcss/postcss': + specifier: ^4 + version: 4.1.17 + '@types/node': + specifier: ^20 + version: 20.19.25 + '@types/react': + specifier: ^19 + version: 19.2.7 + '@types/react-dom': + specifier: ^19 + version: 19.2.3(@types/react@19.2.7) + eslint: + specifier: ^9 + version: 9.39.1(jiti@2.6.1) + eslint-config-next: + specifier: 16.0.5 + version: 16.0.5(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + tailwindcss: + specifier: ^4 + version: 4.1.17 + typescript: + specifier: ^5 + version: 5.9.3 + packages: '@alloc/quick-lru@5.2.0': @@ -435,6 +481,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.1': + resolution: {integrity: sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.18.20': resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} @@ -447,6 +499,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.1': + resolution: {integrity: sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.18.20': resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} @@ -459,6 +517,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.1': + resolution: {integrity: sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.18.20': resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} engines: {node: '>=12'} @@ -471,6 +535,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.1': + resolution: {integrity: sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.18.20': resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} @@ -483,6 +553,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.1': + resolution: {integrity: sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.18.20': resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} engines: {node: '>=12'} @@ -495,6 +571,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.1': + resolution: {integrity: sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.18.20': resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} @@ -507,6 +589,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.1': + resolution: {integrity: sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} engines: {node: '>=12'} @@ -519,6 +607,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.1': + resolution: {integrity: sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.18.20': resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} @@ -531,6 +625,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.1': + resolution: {integrity: sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.18.20': resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} engines: {node: '>=12'} @@ -543,6 +643,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.1': + resolution: {integrity: sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.18.20': resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} engines: {node: '>=12'} @@ -555,6 +661,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.1': + resolution: {integrity: sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.18.20': resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} engines: {node: '>=12'} @@ -567,6 +679,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.1': + resolution: {integrity: sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.18.20': resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} engines: {node: '>=12'} @@ -579,6 +697,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.1': + resolution: {integrity: sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.18.20': resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} engines: {node: '>=12'} @@ -591,6 +715,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.1': + resolution: {integrity: sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.18.20': resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} engines: {node: '>=12'} @@ -603,6 +733,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.1': + resolution: {integrity: sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.18.20': resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} engines: {node: '>=12'} @@ -615,6 +751,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.1': + resolution: {integrity: sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.18.20': resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} engines: {node: '>=12'} @@ -627,12 +769,24 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.1': + resolution: {integrity: sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.1': + resolution: {integrity: sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.18.20': resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} engines: {node: '>=12'} @@ -645,12 +799,24 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.1': + resolution: {integrity: sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.1': + resolution: {integrity: sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.18.20': resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} engines: {node: '>=12'} @@ -663,12 +829,24 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.1': + resolution: {integrity: sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.1': + resolution: {integrity: sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.18.20': resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} @@ -681,6 +859,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.1': + resolution: {integrity: sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.18.20': resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} @@ -693,6 +877,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.1': + resolution: {integrity: sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.18.20': resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} engines: {node: '>=12'} @@ -705,6 +895,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.1': + resolution: {integrity: sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.18.20': resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} engines: {node: '>=12'} @@ -717,6 +913,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.1': + resolution: {integrity: sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1243,6 +1445,22 @@ packages: peerDependencies: typescript: '>=4.8.2' + '@nestjs/serve-static@5.0.4': + resolution: {integrity: sha512-3kO1M9D3vsPyWPFardxIjUYeuolS58PnhCoBTkS7t3BrdZFZCKHnBZ15js+UOzOR2Q6HmD7ssGjLd0DVYVdvOw==} + peerDependencies: + '@fastify/static': ^8.0.4 + '@nestjs/common': ^11.0.2 + '@nestjs/core': ^11.0.2 + express: ^5.0.1 + fastify: ^5.2.1 + peerDependenciesMeta: + '@fastify/static': + optional: true + express: + optional: true + fastify: + optional: true + '@nestjs/testing@11.1.9': resolution: {integrity: sha512-UFxerBDdb0RUNxQNj25pvkvNE7/vxKhXYWBt3QuwBFnYISzRIzhVlyIqLfoV5YI3zV0m0Nn4QAn1KM0zzwfEng==} peerDependencies: @@ -1531,6 +1749,9 @@ packages: '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + '@types/multer@2.0.0': + resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==} + '@types/node@20.19.25': resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==} @@ -2482,6 +2703,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.1: + resolution: {integrity: sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -4265,6 +4491,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 + turbo-darwin-64@2.6.3: resolution: {integrity: sha512-BlJJDc1CQ7SK5Y5qnl7AzpkvKSnpkfPmnA+HeU/sgny3oHZckPV2776ebO2M33CYDSor7+8HQwaodY++IINhYg==} cpu: [x64] @@ -4851,147 +5082,225 @@ snapshots: '@esbuild/aix-ppc64@0.25.12': optional: true + '@esbuild/aix-ppc64@0.27.1': + optional: true + '@esbuild/android-arm64@0.18.20': optional: true '@esbuild/android-arm64@0.25.12': optional: true + '@esbuild/android-arm64@0.27.1': + optional: true + '@esbuild/android-arm@0.18.20': optional: true '@esbuild/android-arm@0.25.12': optional: true + '@esbuild/android-arm@0.27.1': + optional: true + '@esbuild/android-x64@0.18.20': optional: true '@esbuild/android-x64@0.25.12': optional: true + '@esbuild/android-x64@0.27.1': + optional: true + '@esbuild/darwin-arm64@0.18.20': optional: true '@esbuild/darwin-arm64@0.25.12': optional: true + '@esbuild/darwin-arm64@0.27.1': + optional: true + '@esbuild/darwin-x64@0.18.20': optional: true '@esbuild/darwin-x64@0.25.12': optional: true + '@esbuild/darwin-x64@0.27.1': + optional: true + '@esbuild/freebsd-arm64@0.18.20': optional: true '@esbuild/freebsd-arm64@0.25.12': optional: true + '@esbuild/freebsd-arm64@0.27.1': + optional: true + '@esbuild/freebsd-x64@0.18.20': optional: true '@esbuild/freebsd-x64@0.25.12': optional: true + '@esbuild/freebsd-x64@0.27.1': + optional: true + '@esbuild/linux-arm64@0.18.20': optional: true '@esbuild/linux-arm64@0.25.12': optional: true + '@esbuild/linux-arm64@0.27.1': + optional: true + '@esbuild/linux-arm@0.18.20': optional: true '@esbuild/linux-arm@0.25.12': optional: true + '@esbuild/linux-arm@0.27.1': + optional: true + '@esbuild/linux-ia32@0.18.20': optional: true '@esbuild/linux-ia32@0.25.12': optional: true + '@esbuild/linux-ia32@0.27.1': + optional: true + '@esbuild/linux-loong64@0.18.20': optional: true '@esbuild/linux-loong64@0.25.12': optional: true + '@esbuild/linux-loong64@0.27.1': + optional: true + '@esbuild/linux-mips64el@0.18.20': optional: true '@esbuild/linux-mips64el@0.25.12': optional: true + '@esbuild/linux-mips64el@0.27.1': + optional: true + '@esbuild/linux-ppc64@0.18.20': optional: true '@esbuild/linux-ppc64@0.25.12': optional: true + '@esbuild/linux-ppc64@0.27.1': + optional: true + '@esbuild/linux-riscv64@0.18.20': optional: true '@esbuild/linux-riscv64@0.25.12': optional: true + '@esbuild/linux-riscv64@0.27.1': + optional: true + '@esbuild/linux-s390x@0.18.20': optional: true '@esbuild/linux-s390x@0.25.12': optional: true + '@esbuild/linux-s390x@0.27.1': + optional: true + '@esbuild/linux-x64@0.18.20': optional: true '@esbuild/linux-x64@0.25.12': optional: true + '@esbuild/linux-x64@0.27.1': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true + '@esbuild/netbsd-arm64@0.27.1': + optional: true + '@esbuild/netbsd-x64@0.18.20': optional: true '@esbuild/netbsd-x64@0.25.12': optional: true + '@esbuild/netbsd-x64@0.27.1': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true + '@esbuild/openbsd-arm64@0.27.1': + optional: true + '@esbuild/openbsd-x64@0.18.20': optional: true '@esbuild/openbsd-x64@0.25.12': optional: true + '@esbuild/openbsd-x64@0.27.1': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true + '@esbuild/openharmony-arm64@0.27.1': + optional: true + '@esbuild/sunos-x64@0.18.20': optional: true '@esbuild/sunos-x64@0.25.12': optional: true + '@esbuild/sunos-x64@0.27.1': + optional: true + '@esbuild/win32-arm64@0.18.20': optional: true '@esbuild/win32-arm64@0.25.12': optional: true + '@esbuild/win32-arm64@0.27.1': + optional: true + '@esbuild/win32-ia32@0.18.20': optional: true '@esbuild/win32-ia32@0.25.12': optional: true + '@esbuild/win32-ia32@0.27.1': + optional: true + '@esbuild/win32-x64@0.18.20': optional: true '@esbuild/win32-x64@0.25.12': optional: true + '@esbuild/win32-x64@0.27.1': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))': dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -5614,6 +5923,14 @@ snapshots: transitivePeerDependencies: - chokidar + '@nestjs/serve-static@5.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(express@5.1.0)': + dependencies: + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + path-to-regexp: 8.3.0 + optionalDependencies: + express: 5.1.0 + '@nestjs/testing@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)': dependencies: '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -5869,6 +6186,10 @@ snapshots: '@types/methods@1.1.4': {} + '@types/multer@2.0.0': + dependencies: + '@types/express': 5.0.6 + '@types/node@20.19.25': dependencies: undici-types: 6.21.0 @@ -6879,6 +7200,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + esbuild@0.27.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.1 + '@esbuild/android-arm': 0.27.1 + '@esbuild/android-arm64': 0.27.1 + '@esbuild/android-x64': 0.27.1 + '@esbuild/darwin-arm64': 0.27.1 + '@esbuild/darwin-x64': 0.27.1 + '@esbuild/freebsd-arm64': 0.27.1 + '@esbuild/freebsd-x64': 0.27.1 + '@esbuild/linux-arm': 0.27.1 + '@esbuild/linux-arm64': 0.27.1 + '@esbuild/linux-ia32': 0.27.1 + '@esbuild/linux-loong64': 0.27.1 + '@esbuild/linux-mips64el': 0.27.1 + '@esbuild/linux-ppc64': 0.27.1 + '@esbuild/linux-riscv64': 0.27.1 + '@esbuild/linux-s390x': 0.27.1 + '@esbuild/linux-x64': 0.27.1 + '@esbuild/netbsd-arm64': 0.27.1 + '@esbuild/netbsd-x64': 0.27.1 + '@esbuild/openbsd-arm64': 0.27.1 + '@esbuild/openbsd-x64': 0.27.1 + '@esbuild/openharmony-arm64': 0.27.1 + '@esbuild/sunos-x64': 0.27.1 + '@esbuild/win32-arm64': 0.27.1 + '@esbuild/win32-ia32': 0.27.1 + '@esbuild/win32-x64': 0.27.1 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -9029,6 +9379,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.21.0: + dependencies: + esbuild: 0.27.1 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + turbo-darwin-64@2.6.3: optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8ab6766..4ce9e57 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,3 @@ packages: - - 'admin' + - 'frontend' - 'backend'