diff --git a/frontend/components/tests/CookieBanner.test.tsx b/frontend/components/tests/CookieBanner.test.tsx
new file mode 100644
index 00000000..cc747454
--- /dev/null
+++ b/frontend/components/tests/CookieBanner.test.tsx
@@ -0,0 +1,53 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { render, screen, act, fireEvent } from '@testing-library/react';
+import { CookieBanner } from '@/components/shared/CookieBanner';
+
+vi.mock('next-intl', () => ({
+ useTranslations: () => (key: string) => key,
+}));
+
+vi.mock('@/i18n/routing', () => ({
+ Link: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+}));
+
+vi.mock('lucide-react', () => ({
+ X: () => X,
+}));
+
+describe('CookieBanner UI', () => {
+ beforeEach(() => {
+ localStorage.clear();
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('Wait 500ms -> Banner Appears', () => {
+ render();
+ expect(screen.queryByText('title')).toBeNull();
+
+ act(() => {
+ vi.advanceTimersByTime(500);
+ });
+
+ expect(screen.getByText('title')).toBeDefined();
+ });
+
+ it('Click Accept -> Saves to LocalStorage -> Hides Banner', () => {
+ render();
+
+ act(() => {
+ vi.advanceTimersByTime(500);
+ });
+
+ const acceptButton = screen.getByText('accept');
+ fireEvent.click(acceptButton);
+
+ expect(localStorage.getItem('cookie-consent')).toBe('accepted');
+ expect(screen.queryByText('title')).toBeNull();
+ });
+});
diff --git a/frontend/drizzle/0014_steep_kabuki.sql b/frontend/drizzle/0014_steep_kabuki.sql
new file mode 100644
index 00000000..059ad752
--- /dev/null
+++ b/frontend/drizzle/0014_steep_kabuki.sql
@@ -0,0 +1,3 @@
+ALTER TABLE "quiz_attempts" ADD CONSTRAINT "quiz_attempts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "quiz_attempts_user_id_idx" ON "quiz_attempts" USING btree ("user_id");--> statement-breakpoint
+CREATE INDEX "quiz_attempts_quiz_id_idx" ON "quiz_attempts" USING btree ("quiz_id");
\ No newline at end of file
diff --git a/frontend/drizzle/meta/0014_snapshot.json b/frontend/drizzle/meta/0014_snapshot.json
new file mode 100644
index 00000000..80625ee3
--- /dev/null
+++ b/frontend/drizzle/meta/0014_snapshot.json
@@ -0,0 +1,2333 @@
+{
+ "id": "f8cca1fa-e249-4ae5-a962-d7efc1d74194",
+ "prevId": "3de2d313-f082-439b-a8b2-c1fc596a8843",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.categories": {
+ "name": "categories",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_order": {
+ "name": "display_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()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "categories_slug_unique": {
+ "name": "categories_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.category_translations": {
+ "name": "category_translations",
+ "schema": "",
+ "columns": {
+ "category_id": {
+ "name": "category_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "locale": {
+ "name": "locale",
+ "type": "varchar(5)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "category_translations_category_id_categories_id_fk": {
+ "name": "category_translations_category_id_categories_id_fk",
+ "tableFrom": "category_translations",
+ "tableTo": "categories",
+ "columnsFrom": [
+ "category_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "category_translations_category_id_locale_pk": {
+ "name": "category_translations_category_id_locale_pk",
+ "columns": [
+ "category_id",
+ "locale"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.question_translations": {
+ "name": "question_translations",
+ "schema": "",
+ "columns": {
+ "question_id": {
+ "name": "question_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "locale": {
+ "name": "locale",
+ "type": "varchar(5)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "question": {
+ "name": "question",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "answer_blocks": {
+ "name": "answer_blocks",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "question_translations_question_id_questions_id_fk": {
+ "name": "question_translations_question_id_questions_id_fk",
+ "tableFrom": "question_translations",
+ "tableTo": "questions",
+ "columnsFrom": [
+ "question_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "question_translations_question_id_locale_pk": {
+ "name": "question_translations_question_id_locale_pk",
+ "columns": [
+ "question_id",
+ "locale"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.questions": {
+ "name": "questions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "category_id": {
+ "name": "category_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "difficulty": {
+ "name": "difficulty",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'medium'"
+ },
+ "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": {
+ "questions_category_sort_order_idx": {
+ "name": "questions_category_sort_order_idx",
+ "columns": [
+ {
+ "expression": "category_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "questions_category_id_categories_id_fk": {
+ "name": "questions_category_id_categories_id_fk",
+ "tableFrom": "questions",
+ "tableTo": "categories",
+ "columnsFrom": [
+ "category_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.quiz_answer_translations": {
+ "name": "quiz_answer_translations",
+ "schema": "",
+ "columns": {
+ "quiz_answer_id": {
+ "name": "quiz_answer_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "locale": {
+ "name": "locale",
+ "type": "varchar(5)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "answer_text": {
+ "name": "answer_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "quiz_answer_translations_quiz_answer_id_quiz_answers_id_fk": {
+ "name": "quiz_answer_translations_quiz_answer_id_quiz_answers_id_fk",
+ "tableFrom": "quiz_answer_translations",
+ "tableTo": "quiz_answers",
+ "columnsFrom": [
+ "quiz_answer_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "quiz_answer_translations_quiz_answer_id_locale_pk": {
+ "name": "quiz_answer_translations_quiz_answer_id_locale_pk",
+ "columns": [
+ "quiz_answer_id",
+ "locale"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.quiz_answers": {
+ "name": "quiz_answers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "quiz_question_id": {
+ "name": "quiz_question_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_correct": {
+ "name": "is_correct",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {
+ "quiz_answers_question_display_order_idx": {
+ "name": "quiz_answers_question_display_order_idx",
+ "columns": [
+ {
+ "expression": "quiz_question_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "quiz_answers_quiz_question_id_quiz_questions_id_fk": {
+ "name": "quiz_answers_quiz_question_id_quiz_questions_id_fk",
+ "tableFrom": "quiz_answers",
+ "tableTo": "quiz_questions",
+ "columnsFrom": [
+ "quiz_question_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.quiz_attempt_answers": {
+ "name": "quiz_attempt_answers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "attempt_id": {
+ "name": "attempt_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "quiz_question_id": {
+ "name": "quiz_question_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "selected_answer_id": {
+ "name": "selected_answer_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_correct": {
+ "name": "is_correct",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "answered_at": {
+ "name": "answered_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "quiz_attempt_answers_attempt_idx": {
+ "name": "quiz_attempt_answers_attempt_idx",
+ "columns": [
+ {
+ "expression": "attempt_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "quiz_attempt_answers_attempt_id_quiz_attempts_id_fk": {
+ "name": "quiz_attempt_answers_attempt_id_quiz_attempts_id_fk",
+ "tableFrom": "quiz_attempt_answers",
+ "tableTo": "quiz_attempts",
+ "columnsFrom": [
+ "attempt_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "quiz_attempt_answers_quiz_question_id_quiz_questions_id_fk": {
+ "name": "quiz_attempt_answers_quiz_question_id_quiz_questions_id_fk",
+ "tableFrom": "quiz_attempt_answers",
+ "tableTo": "quiz_questions",
+ "columnsFrom": [
+ "quiz_question_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "quiz_attempt_answers_selected_answer_id_quiz_answers_id_fk": {
+ "name": "quiz_attempt_answers_selected_answer_id_quiz_answers_id_fk",
+ "tableFrom": "quiz_attempt_answers",
+ "tableTo": "quiz_answers",
+ "columnsFrom": [
+ "selected_answer_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.quiz_attempts": {
+ "name": "quiz_attempts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "quiz_id": {
+ "name": "quiz_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "score": {
+ "name": "score",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "total_questions": {
+ "name": "total_questions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "percentage": {
+ "name": "percentage",
+ "type": "numeric(5, 2)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "time_spent_seconds": {
+ "name": "time_spent_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "integrity_score": {
+ "name": "integrity_score",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 100
+ },
+ "points_earned": {
+ "name": "points_earned",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'::jsonb"
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "quiz_attempts_user_id_idx": {
+ "name": "quiz_attempts_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "quiz_attempts_quiz_id_idx": {
+ "name": "quiz_attempts_quiz_id_idx",
+ "columns": [
+ {
+ "expression": "quiz_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "quiz_attempts_user_completed_at_idx": {
+ "name": "quiz_attempts_user_completed_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "completed_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "quiz_attempts_quiz_percentage_completed_at_idx": {
+ "name": "quiz_attempts_quiz_percentage_completed_at_idx",
+ "columns": [
+ {
+ "expression": "quiz_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "percentage",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "completed_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "quiz_attempts_quiz_integrity_score_idx": {
+ "name": "quiz_attempts_quiz_integrity_score_idx",
+ "columns": [
+ {
+ "expression": "quiz_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "integrity_score",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "quiz_attempts_user_id_users_id_fk": {
+ "name": "quiz_attempts_user_id_users_id_fk",
+ "tableFrom": "quiz_attempts",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "quiz_attempts_quiz_id_quizzes_id_fk": {
+ "name": "quiz_attempts_quiz_id_quizzes_id_fk",
+ "tableFrom": "quiz_attempts",
+ "tableTo": "quizzes",
+ "columnsFrom": [
+ "quiz_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.quiz_question_content": {
+ "name": "quiz_question_content",
+ "schema": "",
+ "columns": {
+ "quiz_question_id": {
+ "name": "quiz_question_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "locale": {
+ "name": "locale",
+ "type": "varchar(5)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "question_text": {
+ "name": "question_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "explanation": {
+ "name": "explanation",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "quiz_question_content_quiz_question_id_quiz_questions_id_fk": {
+ "name": "quiz_question_content_quiz_question_id_quiz_questions_id_fk",
+ "tableFrom": "quiz_question_content",
+ "tableTo": "quiz_questions",
+ "columnsFrom": [
+ "quiz_question_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "quiz_question_content_quiz_question_id_locale_pk": {
+ "name": "quiz_question_content_quiz_question_id_locale_pk",
+ "columns": [
+ "quiz_question_id",
+ "locale"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.quiz_questions": {
+ "name": "quiz_questions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "quiz_id": {
+ "name": "quiz_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_question_id": {
+ "name": "source_question_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "difficulty": {
+ "name": "difficulty",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'medium'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "quiz_questions_quiz_display_order_idx": {
+ "name": "quiz_questions_quiz_display_order_idx",
+ "columns": [
+ {
+ "expression": "quiz_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "quiz_questions_quiz_id_quizzes_id_fk": {
+ "name": "quiz_questions_quiz_id_quizzes_id_fk",
+ "tableFrom": "quiz_questions",
+ "tableTo": "quizzes",
+ "columnsFrom": [
+ "quiz_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.quiz_translations": {
+ "name": "quiz_translations",
+ "schema": "",
+ "columns": {
+ "quiz_id": {
+ "name": "quiz_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "locale": {
+ "name": "locale",
+ "type": "varchar(5)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "quiz_translations_quiz_id_quizzes_id_fk": {
+ "name": "quiz_translations_quiz_id_quizzes_id_fk",
+ "tableFrom": "quiz_translations",
+ "tableTo": "quizzes",
+ "columnsFrom": [
+ "quiz_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "quiz_translations_quiz_id_locale_pk": {
+ "name": "quiz_translations_quiz_id_locale_pk",
+ "columns": [
+ "quiz_id",
+ "locale"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.quizzes": {
+ "name": "quizzes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "category_id": {
+ "name": "category_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "questions_count": {
+ "name": "questions_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 10
+ },
+ "time_limit_seconds": {
+ "name": "time_limit_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 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": {
+ "quizzes_slug_idx": {
+ "name": "quizzes_slug_idx",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "quizzes_category_id_categories_id_fk": {
+ "name": "quizzes_category_id_categories_id_fk",
+ "tableFrom": "quizzes",
+ "tableTo": "categories",
+ "columnsFrom": [
+ "category_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "quizzes_category_id_slug_unique": {
+ "name": "quizzes_category_id_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "category_id",
+ "slug"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'credentials'"
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'user'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "users_provider_provider_id_unique": {
+ "name": "users_provider_provider_id_unique",
+ "columns": [
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.point_transactions": {
+ "name": "point_transactions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "points": {
+ "name": "points",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'quiz'"
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "point_transactions_user_id_idx": {
+ "name": "point_transactions_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "point_transactions_user_id_users_id_fk": {
+ "name": "point_transactions_user_id_users_id_fk",
+ "tableFrom": "point_transactions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.internal_job_state": {
+ "name": "internal_job_state",
+ "schema": "",
+ "columns": {
+ "job_name": {
+ "name": "job_name",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "next_allowed_at": {
+ "name": "next_allowed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_run_id": {
+ "name": "last_run_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "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.inventory_moves": {
+ "name": "inventory_moves",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "move_key": {
+ "name": "move_key",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "order_id": {
+ "name": "order_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "product_id": {
+ "name": "product_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "inventory_move_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "inventory_moves_move_key_uq": {
+ "name": "inventory_moves_move_key_uq",
+ "columns": [
+ {
+ "expression": "move_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "inventory_moves_order_id_idx": {
+ "name": "inventory_moves_order_id_idx",
+ "columns": [
+ {
+ "expression": "order_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "inventory_moves_product_id_idx": {
+ "name": "inventory_moves_product_id_idx",
+ "columns": [
+ {
+ "expression": "product_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "inventory_moves_order_id_orders_id_fk": {
+ "name": "inventory_moves_order_id_orders_id_fk",
+ "tableFrom": "inventory_moves",
+ "tableTo": "orders",
+ "columnsFrom": [
+ "order_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "inventory_moves_product_id_products_id_fk": {
+ "name": "inventory_moves_product_id_products_id_fk",
+ "tableFrom": "inventory_moves",
+ "tableTo": "products",
+ "columnsFrom": [
+ "product_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "inventory_moves_quantity_gt_0": {
+ "name": "inventory_moves_quantity_gt_0",
+ "value": "\"inventory_moves\".\"quantity\" > 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.order_items": {
+ "name": "order_items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "order_id": {
+ "name": "order_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "product_id": {
+ "name": "product_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "selected_size": {
+ "name": "selected_size",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "selected_color": {
+ "name": "selected_color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "unit_price_minor": {
+ "name": "unit_price_minor",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "line_total_minor": {
+ "name": "line_total_minor",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "unit_price": {
+ "name": "unit_price",
+ "type": "numeric(10, 2)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "line_total": {
+ "name": "line_total",
+ "type": "numeric(10, 2)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "product_title": {
+ "name": "product_title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "product_slug": {
+ "name": "product_slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "product_sku": {
+ "name": "product_sku",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "order_items_order_id_idx": {
+ "name": "order_items_order_id_idx",
+ "columns": [
+ {
+ "expression": "order_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "order_items_order_variant_uq": {
+ "name": "order_items_order_variant_uq",
+ "columns": [
+ {
+ "expression": "order_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "product_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "selected_size",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "selected_color",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "order_items_order_id_orders_id_fk": {
+ "name": "order_items_order_id_orders_id_fk",
+ "tableFrom": "order_items",
+ "tableTo": "orders",
+ "columnsFrom": [
+ "order_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "order_items_product_id_products_id_fk": {
+ "name": "order_items_product_id_products_id_fk",
+ "tableFrom": "order_items",
+ "tableTo": "products",
+ "columnsFrom": [
+ "product_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "order_items_quantity_positive": {
+ "name": "order_items_quantity_positive",
+ "value": "\"order_items\".\"quantity\" > 0"
+ },
+ "order_items_unit_price_minor_non_negative": {
+ "name": "order_items_unit_price_minor_non_negative",
+ "value": "\"order_items\".\"unit_price_minor\" >= 0"
+ },
+ "order_items_line_total_minor_non_negative": {
+ "name": "order_items_line_total_minor_non_negative",
+ "value": "\"order_items\".\"line_total_minor\" >= 0"
+ },
+ "order_items_line_total_consistent": {
+ "name": "order_items_line_total_consistent",
+ "value": "\"order_items\".\"line_total_minor\" = \"order_items\".\"unit_price_minor\" * \"order_items\".\"quantity\""
+ },
+ "order_items_unit_price_mirror_consistent": {
+ "name": "order_items_unit_price_mirror_consistent",
+ "value": "\"order_items\".\"unit_price\" = (\"order_items\".\"unit_price_minor\"::numeric / 100)"
+ },
+ "order_items_line_total_mirror_consistent": {
+ "name": "order_items_line_total_mirror_consistent",
+ "value": "\"order_items\".\"line_total\" = (\"order_items\".\"line_total_minor\"::numeric / 100)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.orders": {
+ "name": "orders",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_amount_minor": {
+ "name": "total_amount_minor",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "total_amount": {
+ "name": "total_amount",
+ "type": "numeric(10, 2)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "currency": {
+ "name": "currency",
+ "type": "currency",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'USD'"
+ },
+ "payment_status": {
+ "name": "payment_status",
+ "type": "payment_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "payment_provider": {
+ "name": "payment_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'stripe'"
+ },
+ "payment_intent_id": {
+ "name": "payment_intent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "psp_charge_id": {
+ "name": "psp_charge_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "psp_payment_method": {
+ "name": "psp_payment_method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "psp_status_reason": {
+ "name": "psp_status_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "psp_metadata": {
+ "name": "psp_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "status": {
+ "name": "status",
+ "type": "order_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'CREATED'"
+ },
+ "inventory_status": {
+ "name": "inventory_status",
+ "type": "inventory_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'none'"
+ },
+ "failure_code": {
+ "name": "failure_code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "failure_message": {
+ "name": "failure_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "idempotency_request_hash": {
+ "name": "idempotency_request_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stock_restored": {
+ "name": "stock_restored",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "restocked_at": {
+ "name": "restocked_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "idempotency_key": {
+ "name": "idempotency_key",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "sweep_claimed_at": {
+ "name": "sweep_claimed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sweep_claim_expires_at": {
+ "name": "sweep_claim_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sweep_run_id": {
+ "name": "sweep_run_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sweep_claimed_by": {
+ "name": "sweep_claimed_by",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "orders_sweep_claim_expires_idx": {
+ "name": "orders_sweep_claim_expires_idx",
+ "columns": [
+ {
+ "expression": "sweep_claim_expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "orders_user_id_users_id_fk": {
+ "name": "orders_user_id_users_id_fk",
+ "tableFrom": "orders",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "orders_idempotency_key_unique": {
+ "name": "orders_idempotency_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "idempotency_key"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "orders_payment_provider_valid": {
+ "name": "orders_payment_provider_valid",
+ "value": "\"orders\".\"payment_provider\" in ('stripe', 'none')"
+ },
+ "orders_total_amount_minor_non_negative": {
+ "name": "orders_total_amount_minor_non_negative",
+ "value": "\"orders\".\"total_amount_minor\" >= 0"
+ },
+ "orders_payment_intent_id_null_when_none": {
+ "name": "orders_payment_intent_id_null_when_none",
+ "value": "\"orders\".\"payment_provider\" <> 'none' OR \"orders\".\"payment_intent_id\" IS NULL"
+ },
+ "orders_psp_fields_null_when_none": {
+ "name": "orders_psp_fields_null_when_none",
+ "value": "\"orders\".\"payment_provider\" <> 'none' OR (\n \"orders\".\"psp_charge_id\" IS NULL AND\n \"orders\".\"psp_payment_method\" IS NULL AND\n \"orders\".\"psp_status_reason\" IS NULL\n )"
+ },
+ "orders_total_amount_mirror_consistent": {
+ "name": "orders_total_amount_mirror_consistent",
+ "value": "\"orders\".\"total_amount\" = (\"orders\".\"total_amount_minor\"::numeric / 100)"
+ },
+ "orders_payment_status_valid_when_none": {
+ "name": "orders_payment_status_valid_when_none",
+ "value": "\"orders\".\"payment_provider\" <> 'none' OR \"orders\".\"payment_status\" in ('paid','failed')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.product_prices": {
+ "name": "product_prices",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "product_id": {
+ "name": "product_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "currency": {
+ "name": "currency",
+ "type": "currency",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "price_minor": {
+ "name": "price_minor",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_price_minor": {
+ "name": "original_price_minor",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "price": {
+ "name": "price",
+ "type": "numeric(10, 2)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_price": {
+ "name": "original_price",
+ "type": "numeric(10, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "product_prices_product_id_idx": {
+ "name": "product_prices_product_id_idx",
+ "columns": [
+ {
+ "expression": "product_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "product_prices_product_currency_uq": {
+ "name": "product_prices_product_currency_uq",
+ "columns": [
+ {
+ "expression": "product_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "currency",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "product_prices_product_id_products_id_fk": {
+ "name": "product_prices_product_id_products_id_fk",
+ "tableFrom": "product_prices",
+ "tableTo": "products",
+ "columnsFrom": [
+ "product_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "product_prices_price_positive": {
+ "name": "product_prices_price_positive",
+ "value": "\"product_prices\".\"price_minor\" > 0"
+ },
+ "product_prices_original_price_valid": {
+ "name": "product_prices_original_price_valid",
+ "value": "\"product_prices\".\"original_price_minor\" is null or \"product_prices\".\"original_price_minor\" > \"product_prices\".\"price_minor\""
+ },
+ "product_prices_price_mirror_consistent": {
+ "name": "product_prices_price_mirror_consistent",
+ "value": "\"product_prices\".\"price\" = (\"product_prices\".\"price_minor\"::numeric / 100)"
+ },
+ "product_prices_original_price_null_coupled": {
+ "name": "product_prices_original_price_null_coupled",
+ "value": "(\"product_prices\".\"original_price_minor\" is null) = (\"product_prices\".\"original_price\" is null)"
+ },
+ "product_prices_original_price_mirror_consistent": {
+ "name": "product_prices_original_price_mirror_consistent",
+ "value": "\"product_prices\".\"original_price_minor\" is null or \"product_prices\".\"original_price\" = (\"product_prices\".\"original_price_minor\"::numeric / 100)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.products": {
+ "name": "products",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_url": {
+ "name": "image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image_public_id": {
+ "name": "image_public_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "price": {
+ "name": "price",
+ "type": "numeric(10, 2)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_price": {
+ "name": "original_price",
+ "type": "numeric(10, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "currency": {
+ "name": "currency",
+ "type": "currency",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'USD'"
+ },
+ "category": {
+ "name": "category",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "colors": {
+ "name": "colors",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::text[]"
+ },
+ "sizes": {
+ "name": "sizes",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::text[]"
+ },
+ "badge": {
+ "name": "badge",
+ "type": "product_badge",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'NONE'"
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "is_featured": {
+ "name": "is_featured",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "stock": {
+ "name": "stock",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "sku": {
+ "name": "sku",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "products_slug_unique": {
+ "name": "products_slug_unique",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "products_stock_non_negative": {
+ "name": "products_stock_non_negative",
+ "value": "\"products\".\"stock\" >= 0"
+ },
+ "products_currency_usd_only": {
+ "name": "products_currency_usd_only",
+ "value": "\"products\".\"currency\" = 'USD'"
+ },
+ "products_price_positive": {
+ "name": "products_price_positive",
+ "value": "\"products\".\"price\" > 0"
+ },
+ "products_original_price_valid": {
+ "name": "products_original_price_valid",
+ "value": "\"products\".\"original_price\" is null or \"products\".\"original_price\" > \"products\".\"price\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.stripe_events": {
+ "name": "stripe_events",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'stripe'"
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payment_intent_id": {
+ "name": "payment_intent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "order_id": {
+ "name": "order_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "event_type": {
+ "name": "event_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payment_status": {
+ "name": "payment_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processed_at": {
+ "name": "processed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "stripe_events_event_id_idx": {
+ "name": "stripe_events_event_id_idx",
+ "columns": [
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "stripe_events_order_id_orders_id_fk": {
+ "name": "stripe_events_order_id_orders_id_fk",
+ "tableFrom": "stripe_events",
+ "tableTo": "orders",
+ "columnsFrom": [
+ "order_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.currency": {
+ "name": "currency",
+ "schema": "public",
+ "values": [
+ "USD",
+ "UAH"
+ ]
+ },
+ "public.inventory_move_type": {
+ "name": "inventory_move_type",
+ "schema": "public",
+ "values": [
+ "reserve",
+ "release"
+ ]
+ },
+ "public.inventory_status": {
+ "name": "inventory_status",
+ "schema": "public",
+ "values": [
+ "none",
+ "reserving",
+ "reserved",
+ "release_pending",
+ "released",
+ "failed"
+ ]
+ },
+ "public.order_status": {
+ "name": "order_status",
+ "schema": "public",
+ "values": [
+ "CREATED",
+ "INVENTORY_RESERVED",
+ "INVENTORY_FAILED",
+ "PAID",
+ "CANCELED"
+ ]
+ },
+ "public.payment_status": {
+ "name": "payment_status",
+ "schema": "public",
+ "values": [
+ "pending",
+ "requires_payment",
+ "paid",
+ "failed",
+ "refunded"
+ ]
+ },
+ "public.product_badge": {
+ "name": "product_badge",
+ "schema": "public",
+ "values": [
+ "NEW",
+ "SALE",
+ "NONE"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/frontend/drizzle/meta/_journal.json b/frontend/drizzle/meta/_journal.json
index 4bccba63..82ee5be6 100644
--- a/frontend/drizzle/meta/_journal.json
+++ b/frontend/drizzle/meta/_journal.json
@@ -99,6 +99,13 @@
"when": 1768162667947,
"tag": "0013_brown_gamora",
"breakpoints": true
+ },
+ {
+ "idx": 14,
+ "version": "7",
+ "when": 1768304911639,
+ "tag": "0014_steep_kabuki",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 3638d0cf..c92df78b 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -43,6 +43,9 @@
"devDependencies": {
"@netlify/plugin-nextjs": "^5.15.1",
"@tailwindcss/postcss": "^4",
+ "@testing-library/dom": "^10.4.1",
+ "@testing-library/react": "^16.3.1",
+ "@testing-library/user-event": "^14.6.1",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",
"@types/react": "^19",
@@ -50,6 +53,7 @@
"drizzle-kit": "^0.31.8",
"eslint": "^9",
"eslint-config-next": "16.0.1",
+ "jsdom": "^27.4.0",
"tailwindcss": "^4",
"ts-node": "^10.9.2",
"tsx": "^4.21.0",
@@ -58,6 +62,12 @@
"vitest": "^4.0.16"
}
},
+ "node_modules/@acemir/cssom": {
+ "version": "0.9.31",
+ "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz",
+ "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==",
+ "dev": true
+ },
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -71,6 +81,56 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz",
+ "integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==",
+ "dev": true,
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.4",
+ "@csstools/css-color-parser": "^3.1.0",
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4",
+ "lru-cache": "^11.2.4"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
+ "version": "11.2.4",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
+ "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
+ "dev": true,
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector": {
+ "version": "6.7.6",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz",
+ "integrity": "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==",
+ "dev": true,
+ "dependencies": {
+ "@asamuzakjp/nwsapi": "^2.3.9",
+ "bidi-js": "^1.0.3",
+ "css-tree": "^3.1.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.2.4"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": {
+ "version": "11.2.4",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
+ "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
+ "dev": true,
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@asamuzakjp/nwsapi": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+ "dev": true
+ },
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -263,6 +323,15 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
+ "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@@ -335,6 +404,135 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "dependencies": {
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.0.25",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.25.tgz",
+ "integrity": "sha512-g0Kw9W3vjx5BEBAF8c5Fm2NcB/Fs8jJXh85aXqwEXiL+tqtOut07TWgyaGzAAfTM+gKckrrncyeGEZPcaRgm2Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@drizzle-team/brocli": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
@@ -1445,6 +1643,23 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@exodus/bytes": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.8.0.tgz",
+ "integrity": "sha512-8JPn18Bcp8Uo1T82gR8lh2guEOa5KKU/IEKvvdp0sgmi7coPBWf1Doi1EXsGZb2ehc8ym/StJCjffYV+ne7sXQ==",
+ "dev": true,
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@exodus/crypto": "^1.0.0-rc.4"
+ },
+ "peerDependenciesMeta": {
+ "@exodus/crypto": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@formatjs/ecma402-abstract": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz",
@@ -3910,6 +4125,74 @@
"tailwindcss": "4.1.18"
}
},
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/dom/node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/@testing-library/react": {
+ "version": "16.3.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.1.tgz",
+ "integrity": "sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@testing-library/user-event": {
+ "version": "14.6.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
+ "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=7.21.4"
+ }
+ },
"node_modules/@tsconfig/node10": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
@@ -3949,6 +4232,12 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true
+ },
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@@ -4786,6 +5075,15 @@
"node": ">=0.4.0"
}
},
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -4803,6 +5101,15 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -5091,6 +5398,15 @@
"bcrypt": "bin/bcrypt"
}
},
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "dev": true,
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
@@ -5344,6 +5660,43 @@
"node": ">= 8"
}
},
+ "node_modules/css-tree": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
+ "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
+ "dev": true,
+ "dependencies": {
+ "mdn-data": "2.12.2",
+ "source-map-js": "^1.0.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/cssstyle": {
+ "version": "5.3.7",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz",
+ "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==",
+ "dev": true,
+ "dependencies": {
+ "@asamuzakjp/css-color": "^4.1.1",
+ "@csstools/css-syntax-patches-for-csstree": "^1.0.21",
+ "css-tree": "^3.1.0",
+ "lru-cache": "^11.2.4"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/cssstyle/node_modules/lru-cache": {
+ "version": "11.2.4",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
+ "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
+ "dev": true,
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -5358,6 +5711,19 @@
"dev": true,
"license": "BSD-2-Clause"
},
+ "node_modules/data-urls": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz",
+ "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==",
+ "dev": true,
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^15.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/data-view-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@@ -5494,6 +5860,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -5527,6 +5902,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true
+ },
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
@@ -5731,6 +6112,18 @@
"node": ">=10.13.0"
}
},
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/es-abstract": {
"version": "1.24.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz",
@@ -7497,6 +7890,44 @@
"hermes-estree": "0.25.1"
}
},
+ "node_modules/html-encoding-sniffer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+ "dev": true,
+ "dependencies": {
+ "@exodus/bytes": "^1.6.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -7834,6 +8265,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true
+ },
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -8052,6 +8489,45 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/jsdom": {
+ "version": "27.4.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz",
+ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==",
+ "dev": true,
+ "dependencies": {
+ "@acemir/cssom": "^0.9.28",
+ "@asamuzakjp/dom-selector": "^6.7.6",
+ "@exodus/bytes": "^1.6.0",
+ "cssstyle": "^5.3.4",
+ "data-urls": "^6.0.0",
+ "decimal.js": "^10.6.0",
+ "html-encoding-sniffer": "^6.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "parse5": "^8.0.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^6.0.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^8.0.0",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^15.1.0",
+ "ws": "^8.18.3",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -8577,6 +9053,15 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -8603,6 +9088,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/mdn-data": {
+ "version": "2.12.2",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
+ "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
+ "dev": true
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -9122,6 +9613,18 @@
"node": ">=6"
}
},
+ "node_modules/parse5": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
+ "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
+ "dev": true,
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -9370,6 +9873,38 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/pretty-format/node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true
+ },
"node_modules/prism-react-renderer": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz",
@@ -9536,6 +10071,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -9738,6 +10282,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -10261,6 +10817,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true
+ },
"node_modules/tailwind-merge": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
@@ -10376,6 +10938,24 @@
"node": ">=14.0.0"
}
},
+ "node_modules/tldts": {
+ "version": "7.0.19",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz",
+ "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==",
+ "dev": true,
+ "dependencies": {
+ "tldts-core": "^7.0.19"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.0.19",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz",
+ "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==",
+ "dev": true
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -10388,6 +10968,30 @@
"node": ">=8.0"
}
},
+ "node_modules/tough-cookie": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
+ "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
+ "dev": true,
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/ts-api-utils": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
@@ -11125,6 +11729,49 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz",
+ "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==",
+ "dev": true,
+ "dependencies": {
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -11257,6 +11904,42 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ws": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
+ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true
+ },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 1cab7605..30b55194 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -13,7 +13,10 @@
"parse": "tsx parse/parse.ts",
"db:generate": "drizzle-kit generate --config drizzle.config.ts",
"db:migrate": "tsx scripts/guard-non-preview.ts drizzle-kit migrate --config drizzle.config.ts",
- "db:studio": "drizzle-kit studio --config drizzle.config.ts"
+ "db:studio": "drizzle-kit studio --config drizzle.config.ts",
+ "test": "vitest",
+ "test:ui": "vitest --ui",
+ "test:run": "vitest run"
},
"dependencies": {
"@neondatabase/serverless": "^1.0.2",
@@ -51,6 +54,9 @@
"devDependencies": {
"@netlify/plugin-nextjs": "^5.15.1",
"@tailwindcss/postcss": "^4",
+ "@testing-library/dom": "^10.4.1",
+ "@testing-library/react": "^16.3.1",
+ "@testing-library/user-event": "^14.6.1",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",
"@types/react": "^19",
@@ -58,6 +64,7 @@
"drizzle-kit": "^0.31.8",
"eslint": "^9",
"eslint-config-next": "16.0.1",
+ "jsdom": "^27.4.0",
"tailwindcss": "^4",
"ts-node": "^10.9.2",
"tsx": "^4.21.0",
diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts
index caea1c4b..b880cfdc 100644
--- a/frontend/vitest.config.ts
+++ b/frontend/vitest.config.ts
@@ -1,18 +1,19 @@
-import { defineConfig } from "vitest/config";
-import { fileURLToPath } from "node:url";
-import path from "node:path";
+import { defineConfig } from 'vitest/config';
+import { fileURLToPath } from 'node:url';
+import path from 'node:path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default defineConfig({
resolve: {
alias: {
- "@": __dirname,
- "server-only": path.join(__dirname, "lib/tests/__mocks__/server-only.ts"),
+ '@': __dirname,
+ 'server-only': path.join(__dirname, 'lib/tests/__mocks__/server-only.ts'),
},
},
test: {
- environment: "node",
- include: ["lib/tests/**/*.test.ts"],
+ environment: 'node',
+ include: ['lib/tests/**/*.test.ts', 'components/tests/**/*.test.tsx'],
+ globals: true,
},
});