From b9525c8e6b6609ae8ef99fabaec7bbbc925c0dca Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 12 Jul 2026 15:27:06 +0200 Subject: [PATCH] feat: add schema for admin content modules --- ...13_admin_events_polls_banners_prefixes.sql | 136 ++++++++++++++++ prisma/schema.prisma | 150 ++++++++++++++++++ .../admin-content-modules-contract.test.ts | 28 ++++ 3 files changed, 314 insertions(+) create mode 100644 prisma/migrations/0013_admin_events_polls_banners_prefixes.sql create mode 100644 src/lib/admin-content-modules-contract.test.ts diff --git a/prisma/migrations/0013_admin_events_polls_banners_prefixes.sql b/prisma/migrations/0013_admin_events_polls_banners_prefixes.sql new file mode 100644 index 0000000000..ab515c59f1 --- /dev/null +++ b/prisma/migrations/0013_admin_events_polls_banners_prefixes.sql @@ -0,0 +1,136 @@ +CREATE TABLE IF NOT EXISTS `website_event_types` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL, + `slug` VARCHAR(100) NOT NULL, + `description` VARCHAR(500) NOT NULL DEFAULT '', + `color` VARCHAR(20) NOT NULL DEFAULT '#3b82f6', + `icon` VARCHAR(50) NOT NULL DEFAULT 'calendar', + `is_active` INTEGER NOT NULL DEFAULT 1, + `min_rank` INTEGER NOT NULL DEFAULT 0, + UNIQUE INDEX `website_event_types_slug_key`(`slug`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `website_events` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `title` VARCHAR(255) NOT NULL, + `description` TEXT NOT NULL, + `type_id` INTEGER NOT NULL, + `host_user_id` INTEGER NOT NULL, + `room_id` INTEGER NULL, + `starts_at` DATETIME(3) NOT NULL, + `ends_at` DATETIME(3) NULL, + `max_players` INTEGER NULL, + `is_recurring` INTEGER NOT NULL DEFAULT 0, + `recurrence_rule` VARCHAR(255) NULL, + `status` VARCHAR(20) NOT NULL DEFAULT 'draft', + `image` VARCHAR(500) NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL, + INDEX `website_events_type_id_idx`(`type_id`), + INDEX `website_events_host_user_id_idx`(`host_user_id`), + INDEX `website_events_starts_at_idx`(`starts_at`), + INDEX `website_events_status_idx`(`status`), + PRIMARY KEY (`id`), + CONSTRAINT `website_events_type_id_fkey` FOREIGN KEY (`type_id`) REFERENCES `website_event_types`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `website_event_registrations` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `event_id` INTEGER NOT NULL, + `user_id` INTEGER NOT NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + INDEX `website_event_registrations_user_id_idx`(`user_id`), + UNIQUE INDEX `website_event_registrations_event_id_user_id_key`(`event_id`, `user_id`), + PRIMARY KEY (`id`), + CONSTRAINT `website_event_registrations_event_id_fkey` FOREIGN KEY (`event_id`) REFERENCES `website_events`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `website_event_prizes` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `event_id` INTEGER NOT NULL, + `position` INTEGER NOT NULL DEFAULT 1, + `prize_type` VARCHAR(20) NOT NULL DEFAULT 'badge', + `badge_code` VARCHAR(50) NULL, + `credits` INTEGER NOT NULL DEFAULT 0, + `pixels` INTEGER NOT NULL DEFAULT 0, + `points` INTEGER NOT NULL DEFAULT 0, + `item_id` INTEGER NULL, + `description` VARCHAR(255) NOT NULL DEFAULT '', + INDEX `website_event_prizes_event_id_idx`(`event_id`), + PRIMARY KEY (`id`), + CONSTRAINT `website_event_prizes_event_id_fkey` FOREIGN KEY (`event_id`) REFERENCES `website_events`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `website_event_winners` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `event_id` INTEGER NOT NULL, + `user_id` INTEGER NOT NULL, + `position` INTEGER NOT NULL DEFAULT 1, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + INDEX `website_event_winners_user_id_idx`(`user_id`), + UNIQUE INDEX `website_event_winners_event_id_user_id_key`(`event_id`, `user_id`), + PRIMARY KEY (`id`), + CONSTRAINT `website_event_winners_event_id_fkey` FOREIGN KEY (`event_id`) REFERENCES `website_events`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `website_polls` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `title` VARCHAR(255) NOT NULL, + `description` TEXT NULL, + `status` VARCHAR(20) NOT NULL DEFAULT 'draft', + `show_results` INTEGER NOT NULL DEFAULT 1, + `multiple_choice` INTEGER NOT NULL DEFAULT 0, + `starts_at` DATETIME(3) NULL, + `ends_at` DATETIME(3) NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL, + INDEX `website_polls_status_idx`(`status`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `website_poll_questions` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `poll_id` INTEGER NOT NULL, + `question` VARCHAR(500) NOT NULL, + `type` VARCHAR(20) NOT NULL DEFAULT 'single', + `sort_order` INTEGER NOT NULL DEFAULT 0, + `options` TEXT NOT NULL, + INDEX `website_poll_questions_poll_id_idx`(`poll_id`), + PRIMARY KEY (`id`), + CONSTRAINT `website_poll_questions_poll_id_fkey` FOREIGN KEY (`poll_id`) REFERENCES `website_polls`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `website_poll_votes` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `question_id` INTEGER NOT NULL, + `user_id` INTEGER NOT NULL, + `answer` VARCHAR(500) NOT NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + INDEX `website_poll_votes_user_id_idx`(`user_id`), + UNIQUE INDEX `website_poll_votes_question_id_user_id_key`(`question_id`, `user_id`), + PRIMARY KEY (`id`), + CONSTRAINT `website_poll_votes_question_id_fkey` FOREIGN KEY (`question_id`) REFERENCES `website_poll_questions`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `website_banners` ( + `id` INT NOT NULL AUTO_INCREMENT, + `title` VARCHAR(255) NOT NULL DEFAULT '', `subtitle` VARCHAR(500) NOT NULL DEFAULT '', + `image` VARCHAR(500) NOT NULL DEFAULT '', `link` VARCHAR(500) NOT NULL DEFAULT '', + `color` VARCHAR(20) NOT NULL DEFAULT '', `is_active` INT NOT NULL DEFAULT 1, + `sort_order` INT NOT NULL DEFAULT 0, `start_date` VARCHAR(50) NULL, `end_date` VARCHAR(50) NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `custom_prefixes` ( + `id` INT NOT NULL AUTO_INCREMENT, `user_id` INT NOT NULL, `text` VARCHAR(50) NOT NULL, + `color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF', `icon` VARCHAR(50) NOT NULL DEFAULT '', + `effect` VARCHAR(50) NOT NULL DEFAULT '', `active` TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), INDEX `idx_custom_prefix_user` (`user_id`, `active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` ( + `id` INT NOT NULL AUTO_INCREMENT, `word` VARCHAR(100) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uq_prefix_word` (`word`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CREATE TABLE IF NOT EXISTS `custom_prefix_settings` ( + `key` VARCHAR(100) NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 68cac4b39e..9da53d6cf0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -2606,3 +2606,153 @@ model RadioAutoDjPlaylist { @@map("radio_auto_dj_playlist") } + +model WebsiteBanner { + id Int @id @default(autoincrement()) + title String @default("") + subtitle String @default("") + image String @default("") @db.VarChar(500) + link String @default("") @db.VarChar(500) + color String @default("") @db.VarChar(20) + isActive Int @default(1) @map("is_active") + sortOrder Int @default(0) @map("sort_order") + startDate String? @map("start_date") @db.VarChar(50) + endDate String? @map("end_date") @db.VarChar(50) + + @@map("website_banners") +} + +model WebsiteEventType { + id Int @id @default(autoincrement()) + name String @db.VarChar(100) + slug String @unique @db.VarChar(100) + description String @default("") @db.VarChar(500) + color String @default("#3b82f6") @db.VarChar(20) + icon String @default("calendar") @db.VarChar(50) + isActive Int @default(1) @map("is_active") + minRank Int @default(0) @map("min_rank") + events WebsiteEvent[] + + @@map("website_event_types") +} + +model WebsiteEvent { + id Int @id @default(autoincrement()) + title String @db.VarChar(255) + description String @db.Text + typeId Int @map("type_id") + hostUserId Int @map("host_user_id") + roomId Int? @map("room_id") + startsAt DateTime @map("starts_at") + endsAt DateTime? @map("ends_at") + maxPlayers Int? @map("max_players") + isRecurring Int @default(0) @map("is_recurring") + recurrenceRule String? @map("recurrence_rule") @db.VarChar(255) + status String @default("draft") @db.VarChar(20) + image String? @db.VarChar(500) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + type WebsiteEventType @relation(fields: [typeId], references: [id]) + registrations WebsiteEventRegistration[] + prizes WebsiteEventPrize[] + winners WebsiteEventWinner[] + + @@index([typeId]) + @@index([hostUserId]) + @@index([startsAt]) + @@index([status]) + @@map("website_events") +} + +model WebsiteEventRegistration { + id Int @id @default(autoincrement()) + eventId Int @map("event_id") + userId Int @map("user_id") + createdAt DateTime @default(now()) @map("created_at") + + event WebsiteEvent @relation(fields: [eventId], references: [id], onDelete: Cascade) + + @@unique([eventId, userId]) + @@index([userId]) + @@map("website_event_registrations") +} + +model WebsiteEventPrize { + id Int @id @default(autoincrement()) + eventId Int @map("event_id") + position Int @default(1) + prizeType String @default("badge") @map("prize_type") @db.VarChar(20) + badgeCode String? @map("badge_code") @db.VarChar(50) + credits Int @default(0) + pixels Int @default(0) + points Int @default(0) + itemId Int? @map("item_id") + description String @default("") @db.VarChar(255) + + event WebsiteEvent @relation(fields: [eventId], references: [id], onDelete: Cascade) + + @@index([eventId]) + @@map("website_event_prizes") +} + +model WebsiteEventWinner { + id Int @id @default(autoincrement()) + eventId Int @map("event_id") + userId Int @map("user_id") + position Int @default(1) + createdAt DateTime @default(now()) @map("created_at") + + event WebsiteEvent @relation(fields: [eventId], references: [id], onDelete: Cascade) + + @@unique([eventId, userId]) + @@index([userId]) + @@map("website_event_winners") +} + +model WebsitePoll { + id Int @id @default(autoincrement()) + title String @db.VarChar(255) + description String? @db.Text + status String @default("draft") @db.VarChar(20) + showResults Int @default(1) @map("show_results") + multipleChoice Int @default(0) @map("multiple_choice") + startsAt DateTime? @map("starts_at") + endsAt DateTime? @map("ends_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + questions WebsitePollQuestion[] + + @@index([status]) + @@map("website_polls") +} + +model WebsitePollQuestion { + id Int @id @default(autoincrement()) + pollId Int @map("poll_id") + question String @db.VarChar(500) + type String @default("single") @db.VarChar(20) + sortOrder Int @default(0) @map("sort_order") + options String @db.Text + + poll WebsitePoll @relation(fields: [pollId], references: [id], onDelete: Cascade) + votes WebsitePollVote[] + + @@index([pollId]) + @@map("website_poll_questions") +} + +model WebsitePollVote { + id Int @id @default(autoincrement()) + questionId Int @map("question_id") + userId Int @map("user_id") + answer String @db.VarChar(500) + createdAt DateTime @default(now()) @map("created_at") + + question WebsitePollQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) + + @@unique([questionId, userId]) + @@index([userId]) + @@map("website_poll_votes") +} diff --git a/src/lib/admin-content-modules-contract.test.ts b/src/lib/admin-content-modules-contract.test.ts new file mode 100644 index 0000000000..dfd50c8411 --- /dev/null +++ b/src/lib/admin-content-modules-contract.test.ts @@ -0,0 +1,28 @@ +import { existsSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +const tables = [ + "website_event_types", "website_events", "website_event_registrations", + "website_event_prizes", "website_event_winners", "website_polls", + "website_poll_questions", "website_poll_votes", "website_banners", + "custom_prefixes", "custom_prefix_blacklist", "custom_prefix_settings", +]; + +describe("admin content module port", () => { + it("owns every required table through schema and migration contracts", () => { + const schema = readFileSync(resolve("prisma/schema.prisma"), "utf8"); + const migration = readFileSync(resolve("prisma/migrations/0013_admin_events_polls_banners_prefixes.sql"), "utf8"); + for (const table of tables) { + expect(migration).toContain(`CREATE TABLE IF NOT EXISTS \`${table}\``); + if (!table.startsWith("custom_prefix")) expect(schema).toContain(`@@map("${table}")`); + } + }); + + it("provides locale-free routes and matching server actions", () => { + for (const module of ["events", "polls", "banners", "prefixes"]) { + expect(existsSync(resolve(`src/app/admin/${module}/page.tsx`))).toBe(true); + expect(existsSync(resolve(`src/actions/${module}.ts`))).toBe(true); + } + }); +});