From 9e16daed67978dc34f94fe9bcd1d7b8232d82019 Mon Sep 17 00:00:00 2001 From: Cris Date: Sun, 4 Jan 2026 06:21:30 -0500 Subject: [PATCH 1/9] fix: add productos table to database schema - Add productos table with id, nombre, precio, categoria columns - Add producto_id foreign key to pedidos table - Insert 8 sample products across categories - Update pedidos records with producto_id references Fixes #2 --- src/lib/db-service.ts | 369 +++++++++++++++++++++++------------------- 1 file changed, 203 insertions(+), 166 deletions(-) diff --git a/src/lib/db-service.ts b/src/lib/db-service.ts index 59f49a1..7368d09 100644 --- a/src/lib/db-service.ts +++ b/src/lib/db-service.ts @@ -1,68 +1,68 @@ -import { PGlite } from '@electric-sql/pglite' -import { handleSQLError } from './sql-error-handler' +import { PGlite } from "@electric-sql/pglite"; +import { handleSQLError } from "./sql-error-handler"; export interface QueryResult { - error: boolean - message?: string - example?: string - rows: Record[] - fields: { name: string }[] + error: boolean; + message?: string; + example?: string; + rows: Record[]; + fields: { name: string }[]; } export interface ColumnInfo { - name: string - type: string - nullable: boolean - defaultValue: string | null + name: string; + type: string; + nullable: boolean; + defaultValue: string | null; } export interface ConstraintInfo { - name: string - type: 'PRIMARY KEY' | 'FOREIGN KEY' | 'UNIQUE' | 'CHECK' | 'NOT NULL' - columns: string[] - definition?: string + name: string; + type: "PRIMARY KEY" | "FOREIGN KEY" | "UNIQUE" | "CHECK" | "NOT NULL"; + columns: string[]; + definition?: string; } export interface IndexInfo { - name: string - columns: string[] - isUnique: boolean + name: string; + columns: string[]; + isUnique: boolean; } export interface TableInfo { - name: string - columns: ColumnInfo[] - constraints: ConstraintInfo[] - indexes: IndexInfo[] + name: string; + columns: ColumnInfo[]; + constraints: ConstraintInfo[]; + indexes: IndexInfo[]; } export interface SchemaInfo { - tables: TableInfo[] + tables: TableInfo[]; } -const DDL_SCHEMA = 'practice_ddl' +const DDL_SCHEMA = "practice_ddl"; class DatabaseService { - private db: PGlite | null = null - private initPromise: Promise | null = null - private ddlSchemaInitialized = false - private ddlResetInProgress = false - private ddlResetPromise: Promise | null = null + private db: PGlite | null = null; + private initPromise: Promise | null = null; + private ddlSchemaInitialized = false; + private ddlResetInProgress = false; + private ddlResetPromise: Promise | null = null; async initialize() { - if (typeof window === 'undefined') { - throw new Error('Database can only be initialized in the browser') + if (typeof window === "undefined") { + throw new Error("Database can only be initialized in the browser"); } - if (this.db) return this.db + if (this.db) return this.db; if (this.initPromise) { - return this.initPromise + return this.initPromise; } this.initPromise = new Promise(async (resolve, reject) => { try { - this.db = new PGlite() + this.db = new PGlite(); await this.db.exec(` CREATE TABLE IF NOT EXISTS usuarios ( @@ -75,14 +75,23 @@ class DatabaseService { activo BOOLEAN ); + CREATE TABLE IF NOT EXISTS productos ( + id SERIAL PRIMARY KEY, + nombre VARCHAR(100), + precio DECIMAL(10,2), + categoria VARCHAR(50) + ); + CREATE TABLE IF NOT EXISTS pedidos ( id SERIAL PRIMARY KEY, usuario_id INTEGER REFERENCES usuarios(id), + producto_id INTEGER REFERENCES productos(id), monto DECIMAL(10,2), fecha DATE ); TRUNCATE TABLE pedidos RESTART IDENTITY; + TRUNCATE TABLE productos RESTART IDENTITY CASCADE; TRUNCATE TABLE usuarios RESTART IDENTITY CASCADE; INSERT INTO usuarios (nombre, email, fecha_registro, edad, ciudad, activo) VALUES @@ -97,101 +106,111 @@ class DatabaseService { ('Carmen Ruiz', 'carmen.ruiz@email.com', '2023-09-14', 27, 'Granada', false), ('Miguel Flores', 'miguel.flores@email.com', '2023-10-25', 36, 'Murcia', true); - INSERT INTO pedidos (usuario_id, monto, fecha) VALUES - (1, 150.50, '2023-02-01'), - (1, 200.75, '2023-03-15'), - (2, 350.00, '2023-02-28'), - (3, 125.25, '2023-04-10'), - (4, 475.00, '2023-05-05'), - (4, 225.50, '2023-06-20'), - (5, 180.75, '2023-07-12'), - (6, 300.00, '2023-08-18'), - (7, 425.25, '2023-09-22'), - (7, 150.00, '2023-10-05'), - (8, 275.50, '2023-11-15'), - (9, 190.75, '2023-12-01'), - (10, 400.00, '2023-12-10'), - (10, 325.25, '2023-12-20'); - `) - - resolve(this.db) + INSERT INTO productos (nombre, precio, categoria) VALUES + ('Laptop Pro', 1299.99, 'Electrónica'), + ('Smartphone X', 899.50, 'Electrónica'), + ('Auriculares Wireless', 149.99, 'Electrónica'), + ('Camiseta Premium', 45.00, 'Ropa'), + ('Zapatillas Running', 120.00, 'Ropa'), + ('Libro SQL Avanzado', 35.50, 'Libros'), + ('Teclado Mecánico', 89.99, 'Electrónica'), + ('Mochila Viaje', 75.00, 'Accesorios'); + + INSERT INTO pedidos (usuario_id, producto_id, monto, fecha) VALUES + (1, 1, 150.50, '2023-02-01'), + (1, 3, 200.75, '2023-03-15'), + (2, 2, 350.00, '2023-02-28'), + (3, 4, 125.25, '2023-04-10'), + (4, 1, 475.00, '2023-05-05'), + (4, 5, 225.50, '2023-06-20'), + (5, 6, 180.75, '2023-07-12'), + (6, 7, 300.00, '2023-08-18'), + (7, 2, 425.25, '2023-09-22'), + (7, 8, 150.00, '2023-10-05'), + (8, 3, 275.50, '2023-11-15'), + (9, 4, 190.75, '2023-12-01'), + (10, 1, 400.00, '2023-12-10'), + (10, 5, 325.25, '2023-12-20'); + `); + + resolve(this.db); } catch (error) { - reject(error) + reject(error); } finally { - this.initPromise = null + this.initPromise = null; } - }) + }); - return this.initPromise + return this.initPromise; } async initializeDDLSchema(): Promise { if (!this.db) { - await this.initialize() + await this.initialize(); } - if (this.ddlSchemaInitialized) return + if (this.ddlSchemaInitialized) return; try { await this.db!.exec(` CREATE SCHEMA IF NOT EXISTS ${DDL_SCHEMA}; SET search_path TO ${DDL_SCHEMA}, public; - `) - this.ddlSchemaInitialized = true + `); + this.ddlSchemaInitialized = true; } catch (error) { - console.error('Error initializing DDL schema:', error) - throw error + console.error("Error initializing DDL schema:", error); + throw error; } } async resetDDLSchema(setupSQL?: string): Promise { // If a reset is already in progress, wait for it to complete if (this.ddlResetInProgress && this.ddlResetPromise) { - console.log('[DDL] Reset already in progress, waiting...') - await this.ddlResetPromise - return + console.log("[DDL] Reset already in progress, waiting..."); + await this.ddlResetPromise; + return; } - this.ddlResetInProgress = true - + this.ddlResetInProgress = true; + this.ddlResetPromise = (async () => { if (!this.db) { - await this.initialize() + await this.initialize(); } try { - console.log('[DDL] Starting schema reset...') - + console.log("[DDL] Starting schema reset..."); + // Drop and recreate schema await this.db!.exec(` DROP SCHEMA IF EXISTS ${DDL_SCHEMA} CASCADE; CREATE SCHEMA ${DDL_SCHEMA}; SET search_path TO ${DDL_SCHEMA}, public; - `) - - console.log('[DDL] Schema reset complete, running setup SQL...') + `); + + console.log("[DDL] Schema reset complete, running setup SQL..."); if (setupSQL) { - await this.db!.exec(setupSQL) - console.log('[DDL] Setup SQL executed successfully') + await this.db!.exec(setupSQL); + console.log("[DDL] Setup SQL executed successfully"); } - this.ddlSchemaInitialized = true + this.ddlSchemaInitialized = true; } catch (error) { - console.error('[DDL] Error resetting DDL schema:', error) - throw error + console.error("[DDL] Error resetting DDL schema:", error); + throw error; } finally { - this.ddlResetInProgress = false - this.ddlResetPromise = null + this.ddlResetInProgress = false; + this.ddlResetPromise = null; } - })() + })(); - await this.ddlResetPromise + await this.ddlResetPromise; } async inspectSchema(): Promise { if (!this.db) { - await this.initialize() + await this.initialize(); } try { @@ -200,12 +219,12 @@ class DatabaseService { FROM information_schema.tables WHERE table_schema = '${DDL_SCHEMA}' ORDER BY table_name - `) + `); - const tables: TableInfo[] = [] + const tables: TableInfo[] = []; for (const row of tablesResult.rows as { table_name: string }[]) { - const tableName = row.table_name + const tableName = row.table_name; const columnsResult = await this.db!.query(` SELECT @@ -216,19 +235,21 @@ class DatabaseService { FROM information_schema.columns WHERE table_schema = '${DDL_SCHEMA}' AND table_name = '${tableName}' ORDER BY ordinal_position - `) - - const columns: ColumnInfo[] = (columnsResult.rows as { - column_name: string - data_type: string - is_nullable: string - column_default: string | null - }[]).map(col => ({ + `); + + const columns: ColumnInfo[] = ( + columnsResult.rows as { + column_name: string; + data_type: string; + is_nullable: string; + column_default: string | null; + }[] + ).map((col) => ({ name: col.column_name, type: col.data_type, - nullable: col.is_nullable === 'YES', + nullable: col.is_nullable === "YES", defaultValue: col.column_default, - })) + })); const constraintsResult = await this.db!.query(` SELECT @@ -242,23 +263,23 @@ class DatabaseService { WHERE tc.table_schema = '${DDL_SCHEMA}' AND tc.table_name = '${tableName}' ORDER BY tc.constraint_name, kcu.ordinal_position - `) + `); - const constraintMap = new Map() + const constraintMap = new Map(); for (const row of constraintsResult.rows as { - constraint_name: string - constraint_type: string - column_name: string + constraint_name: string; + constraint_type: string; + column_name: string; }[]) { - const existing = constraintMap.get(row.constraint_name) + const existing = constraintMap.get(row.constraint_name); if (existing) { - existing.columns.push(row.column_name) + existing.columns.push(row.column_name); } else { constraintMap.set(row.constraint_name, { name: row.constraint_name, - type: row.constraint_type as ConstraintInfo['type'], + type: row.constraint_type as ConstraintInfo["type"], columns: [row.column_name], - }) + }); } } @@ -272,19 +293,19 @@ class DatabaseService { WHERE tc.table_schema = '${DDL_SCHEMA}' AND tc.table_name = '${tableName}' AND tc.constraint_type = 'CHECK' - `) + `); for (const row of checkConstraintsResult.rows as { - constraint_name: string - check_clause: string + constraint_name: string; + check_clause: string; }[]) { - const existing = constraintMap.get(row.constraint_name) + const existing = constraintMap.get(row.constraint_name); if (existing) { - existing.definition = row.check_clause + existing.definition = row.check_clause; } } - const constraints = Array.from(constraintMap.values()) + const constraints = Array.from(constraintMap.values()); const indexesResult = await this.db!.query(` SELECT @@ -301,78 +322,85 @@ class DatabaseService { AND NOT ix.indisprimary GROUP BY i.relname, ix.indisunique ORDER BY i.relname - `) - - const indexes: IndexInfo[] = (indexesResult.rows as { - index_name: string - columns: string[] - is_unique: boolean - }[]).map(idx => ({ + `); + + const indexes: IndexInfo[] = ( + indexesResult.rows as { + index_name: string; + columns: string[]; + is_unique: boolean; + }[] + ).map((idx) => ({ name: idx.index_name, columns: idx.columns, isUnique: idx.is_unique, - })) + })); tables.push({ name: tableName, columns, constraints, indexes, - }) + }); } - return { tables } + return { tables }; } catch (error) { - console.error('Error inspecting schema:', error) - return { tables: [] } + console.error("Error inspecting schema:", error); + return { tables: [] }; } } async executeDDLQuery(query: string): Promise { - if (typeof window === 'undefined') { - throw new Error('Queries can only be executed in the browser') + if (typeof window === "undefined") { + throw new Error("Queries can only be executed in the browser"); } if (!this.db) { return { error: true, - message: 'Base de datos no inicializada', + message: "Base de datos no inicializada", rows: [], fields: [], - } + }; } try { if (!query.trim()) { return { error: true, - message: 'La consulta SQL no puede estar vacía', - example: 'CREATE TABLE productos (id SERIAL PRIMARY KEY, nombre VARCHAR(100))', + message: "La consulta SQL no puede estar vacía", + example: + "CREATE TABLE productos (id SERIAL PRIMARY KEY, nombre VARCHAR(100))", rows: [], fields: [], - } + }; } - await this.db.exec(`SET search_path TO ${DDL_SCHEMA}, public;`) + await this.db.exec(`SET search_path TO ${DDL_SCHEMA}, public;`); try { - const result = await this.db.query(query) + const result = await this.db.query(query); return { error: false, rows: result.rows as Record[], fields: result.fields as { name: string }[], - } + }; } catch (sqlError: unknown) { - console.error('DDL Error:', sqlError) + console.error("DDL Error:", sqlError); - const errorObj = sqlError as { message?: string; stack?: string; code?: string } - const errorMessage = errorObj?.message || 'Error desconocido' + const errorObj = sqlError as { + message?: string; + stack?: string; + code?: string; + }; + const errorMessage = errorObj?.message || "Error desconocido"; const formattedError = handleSQLError({ message: errorMessage, stack: errorObj?.stack, code: errorObj?.code, - }) + }); return { error: true, @@ -380,77 +408,86 @@ class DatabaseService { example: formattedError.example, rows: [], fields: [], - } + }; } } catch { return { error: true, - message: 'Error inesperado al ejecutar la consulta DDL', - example: 'Intenta verificar la sintaxis de tu consulta', + message: "Error inesperado al ejecutar la consulta DDL", + example: "Intenta verificar la sintaxis de tu consulta", rows: [], fields: [], - } + }; } } - async executeTestQuery(query: string): Promise<{ success: boolean; error?: string }> { + async executeTestQuery( + query: string, + ): Promise<{ success: boolean; error?: string }> { if (!this.db) { - return { success: false, error: 'Base de datos no inicializada' } + return { success: false, error: "Base de datos no inicializada" }; } try { - await this.db.exec(`SET search_path TO ${DDL_SCHEMA}, public;`) - await this.db.query(query) - return { success: true } + await this.db.exec(`SET search_path TO ${DDL_SCHEMA}, public;`); + await this.db.query(query); + return { success: true }; } catch (error) { - const errorObj = error as { message?: string } - return { success: false, error: errorObj?.message || 'Error desconocido' } + const errorObj = error as { message?: string }; + return { + success: false, + error: errorObj?.message || "Error desconocido", + }; } } async executeQuery(query: string): Promise { - if (typeof window === 'undefined') { - throw new Error('Queries can only be executed in the browser') + if (typeof window === "undefined") { + throw new Error("Queries can only be executed in the browser"); } if (!this.db) { return { error: true, - message: 'Base de datos no inicializada', + message: "Base de datos no inicializada", rows: [], fields: [], - } + }; } try { if (!query.trim()) { return { error: true, - message: 'La consulta SQL no puede estar vacía', - example: 'SELECT * FROM usuarios', + message: "La consulta SQL no puede estar vacía", + example: "SELECT * FROM usuarios", rows: [], fields: [], - } + }; } try { - const result = await this.db.query(query) + const result = await this.db.query(query); return { error: false, rows: result.rows as Record[], fields: result.fields as { name: string }[], - } + }; } catch (sqlError: unknown) { - console.error('SQL Error:', sqlError) + console.error("SQL Error:", sqlError); - const errorObj = sqlError as { message?: string; stack?: string; code?: string } - const errorMessage = errorObj?.message || 'Error desconocido' + const errorObj = sqlError as { + message?: string; + stack?: string; + code?: string; + }; + const errorMessage = errorObj?.message || "Error desconocido"; const formattedError = handleSQLError({ message: errorMessage, stack: errorObj?.stack, code: errorObj?.code, - }) + }); return { error: true, @@ -458,18 +495,18 @@ class DatabaseService { example: formattedError.example, rows: [], fields: [], - } + }; } } catch { return { error: true, - message: 'Error inesperado al ejecutar la consulta', - example: 'Intenta verificar la sintaxis de tu consulta', + message: "Error inesperado al ejecutar la consulta", + example: "Intenta verificar la sintaxis de tu consulta", rows: [], fields: [], - } + }; } } } -export const dbService = new DatabaseService() +export const dbService = new DatabaseService(); From 12bdad545dee10e10a2c0820cc71a1bd089873e8 Mon Sep 17 00:00:00 2001 From: Cris Date: Sun, 4 Jan 2026 06:21:36 -0500 Subject: [PATCH 2/9] chore: add vitest testing framework - Install vitest dependency - Add test and test:watch npm scripts - Configure vitest with node environment and path aliases --- bun.lock | 155 ++++++++++++++++++++++++++++++++++++++++++++++- package.json | 5 +- vitest.config.ts | 16 +++++ 3 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 vitest.config.ts diff --git a/bun.lock b/bun.lock index b1f8561..7af0320 100644 --- a/bun.lock +++ b/bun.lock @@ -64,6 +64,7 @@ "tailwindcss": "^3.4.1", "tsx": "^4.21.0", "typescript": "^5.6.3", + "vitest": "^4.0.16", }, }, }, @@ -376,6 +377,50 @@ "@resvg/resvg-js-win32-x64-msvc": ["@resvg/resvg-js-win32-x64-msvc@2.6.2", "", { "os": "win32", "cpu": "x64" }, "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.54.0", "", { "os": "android", "cpu": "arm" }, "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.54.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.54.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.54.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.54.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.54.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.54.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.54.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.54.0", "", { "os": "none", "cpu": "arm64" }, "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.54.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.54.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg=="], + "@shuding/opentype.js": ["@shuding/opentype.js@1.4.0-beta.0", "", { "dependencies": { "fflate": "^0.7.3", "string.prototype.codepointat": "^0.2.1" }, "bin": { "ot": "bin/ot" } }, "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA=="], "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], @@ -392,8 +437,12 @@ "@types/canvas-confetti": ["@types/canvas-confetti@1.9.0", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], @@ -422,6 +471,20 @@ "@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="], + "@vitest/expect": ["@vitest/expect@4.0.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA=="], + + "@vitest/mocker": ["@vitest/mocker@4.0.16", "", { "dependencies": { "@vitest/spy": "4.0.16", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.0.16", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA=="], + + "@vitest/runner": ["@vitest/runner@4.0.16", "", { "dependencies": { "@vitest/utils": "4.0.16", "pathe": "^2.0.3" } }, "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA=="], + + "@vitest/spy": ["@vitest/spy@4.0.16", "", {}, "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw=="], + + "@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="], + "ai": ["ai@6.0.3", "", { "dependencies": { "@ai-sdk/gateway": "3.0.2", "@ai-sdk/provider": "3.0.0", "@ai-sdk/provider-utils": "4.0.1", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OOo+/C+sEyscoLnbY3w42vjQDICioVNyS+F+ogwq6O5RJL/vgWGuiLzFwuP7oHTeni/MkmX8tIge48GTdaV7QQ=="], "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], @@ -434,6 +497,8 @@ "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], @@ -460,6 +525,8 @@ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], @@ -530,6 +597,8 @@ "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], @@ -540,8 +609,12 @@ "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -628,6 +701,8 @@ "lucide-react": ["lucide-react@0.453.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, "sha512-kL+RGZCcJi9BvJtzg2kshO192Ddy9hv3ij+cPrVPWSRzgCWCVazoQJxOjAwgK53NomL07HB7GPHW120FimjNhQ=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], @@ -724,6 +799,8 @@ "obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="], + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], "parse-css-color": ["parse-css-color@0.2.1", "", { "dependencies": { "color-name": "^1.1.4", "hex-rgb": "^4.1.0" } }, "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg=="], @@ -732,6 +809,8 @@ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], "pg-numeric": ["pg-numeric@1.0.2", "", {}, "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw=="], @@ -742,7 +821,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], @@ -808,6 +887,8 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "satori": ["satori@0.18.3", "", { "dependencies": { "@shuding/opentype.js": "1.4.0-beta.0", "css-background-parser": "^0.1.0", "css-box-shadow": "1.0.0-3", "css-gradient-parser": "^0.0.17", "css-to-react-native": "^3.0.0", "emoji-regex-xs": "^2.0.1", "escape-html": "^1.0.3", "linebreak": "^1.1.0", "parse-css-color": "^0.2.1", "postcss-value-parser": "^4.2.0", "yoga-layout": "^3.2.1" } }, "sha512-T3DzWNmnrfVmk2gCIlAxLRLbGkfp3K7TyRva+Byyojqu83BNvnMeqVeYRdmUw4TKCsyH4RiQ/KuF/I4yEzgR5A=="], @@ -824,6 +905,8 @@ "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "sonner": ["sonner@1.7.4", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw=="], "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -836,6 +919,8 @@ "sql-formatter": ["sql-formatter@15.6.12", "", { "dependencies": { "argparse": "^2.0.1", "nearley": "^2.20.1" }, "bin": { "sql-formatter": "bin/sql-formatter-cli.cjs" } }, "sha512-mkpF+RG402P66VMsnQkWewTRzDBWfu9iLbOfxaW/nAKOS/2A9MheQmcU5cmX0D0At9azrorZwpvcBRNNBozACQ=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + "standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="], "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], @@ -874,8 +959,14 @@ "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], @@ -920,8 +1011,14 @@ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + "vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], + + "vitest": ["vitest@4.0.16", "", { "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", "@vitest/pretty-format": "4.0.16", "@vitest/runner": "4.0.16", "@vitest/snapshot": "4.0.16", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.16", "@vitest/browser-preview": "4.0.16", "@vitest/browser-webdriverio": "4.0.16", "@vitest/ui": "4.0.16", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q=="], + "which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -952,12 +1049,16 @@ "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "jsonp/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "nearley/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], @@ -966,12 +1067,14 @@ "postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], - "tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], "tsx/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "vite/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], @@ -1063,5 +1166,51 @@ "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], } } diff --git a/package.json b/package.json index c0139c7..b28da70 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "lint": "biome check .", "lint:fix": "biome check --write .", "format": "biome format --write .", + "test": "vitest run", + "test:watch": "vitest", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:push": "drizzle-kit push", @@ -77,7 +79,8 @@ "satori": "^0.18.3", "tailwindcss": "^3.4.1", "tsx": "^4.21.0", - "typescript": "^5.6.3" + "typescript": "^5.6.3", + "vitest": "^4.0.16" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..76d4ac1 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,16 @@ +import { resolve } from "node:path"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/__tests__/**/*.test.ts"], + globals: true, + testTimeout: 30000, + }, + resolve: { + alias: { + "@": resolve(__dirname, "./src"), + }, + }, +}); From 6db690f62b1b3ffd5d9fee5a1538d9d03005e0ae Mon Sep 17 00:00:00 2001 From: Cris Date: Sun, 4 Jan 2026 06:21:42 -0500 Subject: [PATCH 3/9] test: add test fixtures for exercise validation - Add db-schema.ts with table/column definitions - Add sample-solutions.ts with 50 SQL solutions for all exercises --- src/__tests__/fixtures/db-schema.ts | 43 +++ src/__tests__/fixtures/sample-solutions.ts | 323 +++++++++++++++++++++ 2 files changed, 366 insertions(+) create mode 100644 src/__tests__/fixtures/db-schema.ts create mode 100644 src/__tests__/fixtures/sample-solutions.ts diff --git a/src/__tests__/fixtures/db-schema.ts b/src/__tests__/fixtures/db-schema.ts new file mode 100644 index 0000000..149741d --- /dev/null +++ b/src/__tests__/fixtures/db-schema.ts @@ -0,0 +1,43 @@ +/** + * Database schema definition for testing + * This should mirror the schema in db-service.ts initialize() method + */ + +export interface TableSchema { + name: string; + columns: string[]; +} + +export const DB_TABLES: TableSchema[] = [ + { + name: "usuarios", + columns: [ + "id", + "nombre", + "email", + "fecha_registro", + "edad", + "ciudad", + "activo", + ], + }, + { + name: "productos", + columns: ["id", "nombre", "precio", "categoria"], + }, + { + name: "pedidos", + columns: ["id", "usuario_id", "producto_id", "monto", "fecha"], + }, +]; + +export const ALL_TABLE_NAMES = DB_TABLES.map((t) => t.name); + +export function getTableColumns(tableName: string): string[] { + const table = DB_TABLES.find((t) => t.name === tableName); + return table?.columns ?? []; +} + +export function tableExists(tableName: string): boolean { + return ALL_TABLE_NAMES.includes(tableName); +} diff --git a/src/__tests__/fixtures/sample-solutions.ts b/src/__tests__/fixtures/sample-solutions.ts new file mode 100644 index 0000000..0746ed0 --- /dev/null +++ b/src/__tests__/fixtures/sample-solutions.ts @@ -0,0 +1,323 @@ +/** + * Sample SQL solutions for each exercise + * These are used to verify exercises are solvable with the current schema + */ + +export interface SampleSolution { + exerciseTitle: string; + sql: string; + type: "dml" | "ddl"; +} + +export const SAMPLE_SOLUTIONS: SampleSolution[] = [ + // DML - Principiante + { + exerciseTitle: "Consulta Básica de Selección", + sql: "SELECT * FROM usuarios LIMIT 5", + type: "dml", + }, + { + exerciseTitle: "Selección de Columnas Específicas", + sql: "SELECT nombre, email FROM usuarios", + type: "dml", + }, + { + exerciseTitle: "Filtrado con WHERE", + sql: "SELECT * FROM usuarios WHERE edad > 30", + type: "dml", + }, + { + exerciseTitle: "Ordenamiento con ORDER BY", + sql: "SELECT * FROM usuarios ORDER BY nombre ASC", + type: "dml", + }, + { + exerciseTitle: "Uso de Alias (AS)", + sql: "SELECT nombre AS nombre_usuario, email AS correo FROM usuarios", + type: "dml", + }, + { + exerciseTitle: "Manejo de NULL", + sql: "SELECT * FROM usuarios WHERE ciudad IS NULL", + type: "dml", + }, + { + exerciseTitle: "Ordenamiento Descendente", + sql: "SELECT * FROM usuarios ORDER BY edad DESC", + type: "dml", + }, + { + exerciseTitle: "Operadores Lógicos (AND/OR)", + sql: "SELECT * FROM usuarios WHERE edad > 25 AND ciudad = 'Madrid'", + type: "dml", + }, + { + exerciseTitle: "Operador NOT", + sql: "SELECT * FROM usuarios WHERE NOT activo", + type: "dml", + }, + + // DML - Intermedio + { + exerciseTitle: "Filtrado con Fechas", + sql: "SELECT * FROM usuarios WHERE fecha_registro > '2023-05-10'", + type: "dml", + }, + { + exerciseTitle: "Conteo con COUNT", + sql: "SELECT COUNT(*) FROM usuarios", + type: "dml", + }, + { + exerciseTitle: "Operador LIKE", + sql: "SELECT * FROM usuarios WHERE email LIKE '%gmail%'", + type: "dml", + }, + { + exerciseTitle: "Agrupamiento con GROUP BY", + sql: "SELECT ciudad, COUNT(*) as count FROM usuarios GROUP BY ciudad", + type: "dml", + }, + { + exerciseTitle: "DISTINCT - Valores Únicos", + sql: "SELECT DISTINCT ciudad FROM usuarios", + type: "dml", + }, + { + exerciseTitle: "Operador IN", + sql: "SELECT * FROM usuarios WHERE ciudad IN ('Madrid', 'Barcelona', 'Valencia')", + type: "dml", + }, + { + exerciseTitle: "BETWEEN - Rango de Valores", + sql: "SELECT * FROM usuarios WHERE edad BETWEEN 25 AND 35", + type: "dml", + }, + { + exerciseTitle: "HAVING - Filtrar Agregaciones", + sql: "SELECT ciudad, COUNT(*) as count FROM usuarios GROUP BY ciudad HAVING COUNT(*) > 1", + type: "dml", + }, + { + exerciseTitle: "LOWER y UPPER - Transformar Texto", + sql: "SELECT UPPER(nombre), LOWER(email) FROM usuarios", + type: "dml", + }, + { + exerciseTitle: "ROUND - Redondear Números", + sql: "SELECT ROUND(AVG(monto), 2) FROM pedidos", + type: "dml", + }, + { + exerciseTitle: "Promedio con AVG", + sql: "SELECT AVG(edad) FROM usuarios", + type: "dml", + }, + { + exerciseTitle: "MIN y MAX", + sql: "SELECT MIN(edad), MAX(edad) FROM usuarios", + type: "dml", + }, + { + exerciseTitle: "COALESCE - Valores por Defecto", + sql: "SELECT nombre, COALESCE(ciudad, 'Sin ciudad') as ciudad FROM usuarios", + type: "dml", + }, + { + exerciseTitle: "Concatenación de Texto", + sql: "SELECT nombre || ' (' || email || ')' AS info_usuario FROM usuarios", + type: "dml", + }, + + // DML - Avanzado + { + exerciseTitle: "JOIN Básico", + sql: "SELECT u.nombre, p.monto FROM usuarios u JOIN pedidos p ON u.id = p.usuario_id", + type: "dml", + }, + { + exerciseTitle: "Suma con SUM", + sql: "SELECT SUM(monto) FROM pedidos", + type: "dml", + }, + { + exerciseTitle: "Total de Pedidos por Usuario", + sql: `SELECT u.nombre, u.email, COUNT(p.id) as total_pedidos + FROM usuarios u + LEFT JOIN pedidos p ON u.id = p.usuario_id + GROUP BY u.id, u.nombre, u.email + ORDER BY u.nombre`, + type: "dml", + }, + { + exerciseTitle: "Subconsulta Básica", + sql: `SELECT DISTINCT u.* FROM usuarios u + JOIN pedidos p ON u.id = p.usuario_id + WHERE p.monto > (SELECT AVG(monto) FROM pedidos)`, + type: "dml", + }, + { + exerciseTitle: "Múltiples JOINs", + sql: `SELECT u.nombre, p.monto, pr.nombre as producto + FROM usuarios u + JOIN pedidos p ON u.id = p.usuario_id + JOIN productos pr ON p.producto_id = pr.id`, + type: "dml", + }, + { + exerciseTitle: "LEFT JOIN con NULL", + sql: `SELECT u.nombre, u.email + FROM usuarios u + LEFT JOIN pedidos p ON u.id = p.usuario_id + WHERE p.id IS NULL`, + type: "dml", + }, + { + exerciseTitle: "UNION - Combinar Resultados", + sql: "SELECT nombre FROM usuarios UNION SELECT nombre FROM productos", + type: "dml", + }, + { + exerciseTitle: "CASE WHEN - Expresiones Condicionales", + sql: `SELECT nombre, + CASE + WHEN edad < 30 THEN 'Joven' + WHEN edad BETWEEN 30 AND 50 THEN 'Adulto' + ELSE 'Senior' + END as categoria_edad + FROM usuarios`, + type: "dml", + }, + { + exerciseTitle: "Self JOIN - Auto-unión", + sql: `SELECT u1.nombre, u2.nombre, u1.ciudad + FROM usuarios u1 + JOIN usuarios u2 ON u1.ciudad = u2.ciudad + WHERE u1.id < u2.id`, + type: "dml", + }, + { + exerciseTitle: "NOT IN con Subconsulta", + sql: `SELECT * FROM usuarios + WHERE id NOT IN (SELECT usuario_id FROM pedidos)`, + type: "dml", + }, + { + exerciseTitle: "EXISTS - Verificar Existencia", + sql: `SELECT * FROM usuarios u + WHERE EXISTS (SELECT 1 FROM pedidos p WHERE p.usuario_id = u.id)`, + type: "dml", + }, + { + exerciseTitle: "Subconsulta Correlacionada", + sql: `SELECT nombre, + (SELECT SUM(monto) FROM pedidos WHERE usuario_id = usuarios.id) as total + FROM usuarios`, + type: "dml", + }, + { + exerciseTitle: "ROW_NUMBER - Funciones de Ventana", + sql: `SELECT nombre, fecha_registro, + ROW_NUMBER() OVER (ORDER BY fecha_registro) as numero_fila + FROM usuarios`, + type: "dml", + }, + { + exerciseTitle: "Pedido Máximo por Usuario", + sql: `SELECT u.nombre, MAX(p.monto) as max_monto + FROM usuarios u + JOIN pedidos p ON u.id = p.usuario_id + GROUP BY u.id, u.nombre`, + type: "dml", + }, + + // DDL - Principiante + { + exerciseTitle: "CREATE TABLE - Tabla Básica", + sql: `CREATE TABLE productos ( + id SERIAL PRIMARY KEY, + nombre VARCHAR(100), + precio DECIMAL(10,2) + )`, + type: "ddl", + }, + { + exerciseTitle: "DROP TABLE - Eliminar Tabla", + sql: "DROP TABLE IF EXISTS temporal", + type: "ddl", + }, + { + exerciseTitle: "ALTER TABLE - Agregar Columna", + sql: "ALTER TABLE clientes ADD COLUMN email VARCHAR(255)", + type: "ddl", + }, + { + exerciseTitle: "ALTER TABLE - Eliminar Columna", + sql: "ALTER TABLE inventario DROP COLUMN obsoleto", + type: "ddl", + }, + + // DDL - Intermedio + { + exerciseTitle: "CREATE TABLE con PRIMARY KEY", + sql: `CREATE TABLE empleados ( + id INTEGER PRIMARY KEY, + nombre VARCHAR(100) NOT NULL, + departamento VARCHAR(50) + )`, + type: "ddl", + }, + { + exerciseTitle: "ALTER TABLE - Agregar PRIMARY KEY", + sql: "ALTER TABLE categorias ADD CONSTRAINT pk_categorias PRIMARY KEY (codigo)", + type: "ddl", + }, + { + exerciseTitle: "ALTER TABLE - Agregar FOREIGN KEY", + sql: "ALTER TABLE articulos ADD CONSTRAINT fk_articulos_categoria FOREIGN KEY (categoria_id) REFERENCES categorias(id)", + type: "ddl", + }, + { + exerciseTitle: "CREATE TABLE con NOT NULL", + sql: `CREATE TABLE ordenes ( + id SERIAL PRIMARY KEY, + cliente VARCHAR(100) NOT NULL, + total DECIMAL(10,2) NOT NULL, + fecha DATE + )`, + type: "ddl", + }, + + // DDL - Avanzado + { + exerciseTitle: "ALTER TABLE - Agregar UNIQUE", + sql: "ALTER TABLE usuarios_app ADD CONSTRAINT unique_email UNIQUE (email)", + type: "ddl", + }, + { + exerciseTitle: "ALTER TABLE - Agregar CHECK", + sql: "ALTER TABLE productos_venta ADD CONSTRAINT check_precio_positivo CHECK (precio > 0)", + type: "ddl", + }, + { + exerciseTitle: "CREATE INDEX - Índice Simple", + sql: "CREATE INDEX idx_ventas_fecha ON ventas (fecha)", + type: "ddl", + }, + { + exerciseTitle: "CREATE INDEX - Índice Compuesto", + sql: "CREATE INDEX idx_logs_usuario_fecha ON logs (usuario_id, fecha)", + type: "ddl", + }, + { + exerciseTitle: "ALTER TABLE - Renombrar Columna", + sql: "ALTER TABLE contactos RENAME COLUMN nombre_completo TO nombre", + type: "ddl", + }, +]; + +export function getSolutionForExercise( + title: string, +): SampleSolution | undefined { + return SAMPLE_SOLUTIONS.find((s) => s.exerciseTitle === title); +} From a8c39f092014188d139126c67150bb9076421fee Mon Sep 17 00:00:00 2001 From: Cris Date: Sun, 4 Jan 2026 06:21:47 -0500 Subject: [PATCH 4/9] test: add exercise validation tests - Add schema consistency tests (71 tests) - Add solution execution tests (56 tests) - Validate table/column references exist in DB schema - Verify sample solutions execute successfully against PGlite --- src/__tests__/exercises-schema.test.ts | 201 +++++++++++++++++++ src/__tests__/exercises-solutions.test.ts | 228 ++++++++++++++++++++++ 2 files changed, 429 insertions(+) create mode 100644 src/__tests__/exercises-schema.test.ts create mode 100644 src/__tests__/exercises-solutions.test.ts diff --git a/src/__tests__/exercises-schema.test.ts b/src/__tests__/exercises-schema.test.ts new file mode 100644 index 0000000..ca00b47 --- /dev/null +++ b/src/__tests__/exercises-schema.test.ts @@ -0,0 +1,201 @@ +import { describe, expect, it } from "vitest"; +import { exercisesData } from "@/lib/db/seed-data"; +import { + ALL_TABLE_NAMES, + getTableColumns, + tableExists, +} from "./fixtures/db-schema"; + +/** + * Extract table names from text using common SQL patterns + */ +function extractTableReferences(text: string): string[] { + const patterns = [ + /FROM\s+['"]?(\w+)['"]?/gi, + /JOIN\s+['"]?(\w+)['"]?/gi, + /INTO\s+['"]?(\w+)['"]?/gi, + /UPDATE\s+['"]?(\w+)['"]?/gi, + /TABLE\s+['"]?(\w+)['"]?/gi, + /tabla\s+['"]?(\w+)['"]?/gi, + /tablas?\s+['"]?(\w+)['"]?(?:\s*[,y]\s*['"]?(\w+)['"]?)?(?:\s*[,y]\s*['"]?(\w+)['"]?)?/gi, + ]; + + const tables = new Set(); + + for (const pattern of patterns) { + const regex = new RegExp(pattern.source, pattern.flags); + const matches = text.matchAll(regex); + for (const match of matches) { + for (let i = 1; i < match.length; i++) { + if (match[i]) { + const tableName = match[i].toLowerCase(); + if ( + ALL_TABLE_NAMES.includes(tableName) || + isExpectedTable(tableName) + ) { + tables.add(tableName); + } + } + } + } + } + + return Array.from(tables); +} + +/** + * Check if a table name is one we expect to exist (either in DB or created by DDL exercises) + */ +function isExpectedTable(name: string): boolean { + const expectedTables = [ + ...ALL_TABLE_NAMES, + // DDL exercises may create these tables + "temporal", + "clientes", + "inventario", + "empleados", + "categorias", + "articulos", + "ordenes", + "usuarios_app", + "productos_venta", + "ventas", + "logs", + "contactos", + ]; + return expectedTables.includes(name); +} + +describe("Exercise Schema Consistency", () => { + describe("DML Exercises - Table References", () => { + const dmlExercises = exercisesData.filter((ex) => ex.type !== "ddl"); + + it.each( + dmlExercises.map((ex, idx) => [idx + 1, ex.title, ex]), + )('Exercise #%i "%s" references only existing tables', (_idx, _title, exercise) => { + const textToCheck = [ + exercise.description, + exercise.details, + exercise.hint, + exercise.example?.entrada ?? "", + exercise.example?.salida ?? "", + ].join(" "); + + const referencedTables = extractTableReferences(textToCheck); + + for (const table of referencedTables) { + expect( + tableExists(table), + `Table "${table}" referenced in exercise "${exercise.title}" does not exist in DB schema. Available tables: ${ALL_TABLE_NAMES.join(", ")}`, + ).toBe(true); + } + }); + }); + + describe("Exercise Validation Conditions - Column References", () => { + const exercisesWithColumns = exercisesData.filter( + (ex) => ex.validation?.conditions?.columns && ex.type !== "ddl", + ); + + it.each( + exercisesWithColumns.map((ex, idx) => [idx + 1, ex.title, ex]), + )('Exercise #%i "%s" validation columns exist in schema', (_idx, _title, exercise) => { + const validationColumns = exercise.validation.conditions + .columns as string[]; + + // For most exercises, columns should exist in one of our tables + // We check against all tables since the exercise might join multiple + const allAvailableColumns = ALL_TABLE_NAMES.flatMap((table) => + getTableColumns(table), + ); + + // Also allow computed/aliased columns + const allowedAliases = [ + "total_pedidos", + "count", + "total", + "max_monto", + "numero_fila", + "nombre_usuario", + "correo", + "info_usuario", + "categoria_edad", + "producto", + ]; + + for (const col of validationColumns) { + const isValidColumn = + allAvailableColumns.includes(col) || allowedAliases.includes(col); + expect( + isValidColumn, + `Column "${col}" in exercise "${exercise.title}" validation is not in schema or allowed aliases`, + ).toBe(true); + } + }); + }); + + describe("Core Tables Existence", () => { + it("should have usuarios table with required columns", () => { + expect(tableExists("usuarios")).toBe(true); + const columns = getTableColumns("usuarios"); + expect(columns).toContain("id"); + expect(columns).toContain("nombre"); + expect(columns).toContain("email"); + }); + + it("should have productos table with required columns", () => { + expect(tableExists("productos")).toBe(true); + const columns = getTableColumns("productos"); + expect(columns).toContain("id"); + expect(columns).toContain("nombre"); + expect(columns).toContain("precio"); + }); + + it("should have pedidos table with required columns including producto_id", () => { + expect(tableExists("pedidos")).toBe(true); + const columns = getTableColumns("pedidos"); + expect(columns).toContain("id"); + expect(columns).toContain("usuario_id"); + expect(columns).toContain("producto_id"); + expect(columns).toContain("monto"); + }); + }); + + describe("Multiple JOINs Exercise (#28)", () => { + it("should have all required tables for the Multiple JOINs exercise", () => { + const multipleJoinsExercise = exercisesData.find( + (ex) => ex.title === "Múltiples JOINs", + ); + + expect(multipleJoinsExercise).toBeDefined(); + + // This exercise requires usuarios, pedidos, and productos + expect(tableExists("usuarios")).toBe(true); + expect(tableExists("pedidos")).toBe(true); + expect(tableExists("productos")).toBe(true); + + // pedidos must have foreign keys to both + const pedidosColumns = getTableColumns("pedidos"); + expect(pedidosColumns).toContain("usuario_id"); + expect(pedidosColumns).toContain("producto_id"); + }); + }); + + describe("UNION Exercise (#31)", () => { + it("should have productos table for UNION exercise", () => { + const unionExercise = exercisesData.find( + (ex) => ex.title === "UNION - Combinar Resultados", + ); + + expect(unionExercise).toBeDefined(); + + // This exercise combines usuarios and productos + expect(tableExists("usuarios")).toBe(true); + expect(tableExists("productos")).toBe(true); + + // Both should have nombre column + expect(getTableColumns("usuarios")).toContain("nombre"); + expect(getTableColumns("productos")).toContain("nombre"); + }); + }); +}); diff --git a/src/__tests__/exercises-solutions.test.ts b/src/__tests__/exercises-solutions.test.ts new file mode 100644 index 0000000..610c218 --- /dev/null +++ b/src/__tests__/exercises-solutions.test.ts @@ -0,0 +1,228 @@ +import { PGlite } from "@electric-sql/pglite"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { exercisesData } from "@/lib/db/seed-data"; +import { + getSolutionForExercise, + SAMPLE_SOLUTIONS, +} from "./fixtures/sample-solutions"; + +const DDL_SCHEMA = "practice_ddl"; + +/** + * Initialize a PGlite instance with the full schema + */ +async function createTestDatabase(): Promise { + const db = new PGlite(); + + await db.exec(` + CREATE TABLE IF NOT EXISTS usuarios ( + id SERIAL PRIMARY KEY, + nombre VARCHAR(100), + email VARCHAR(100), + fecha_registro DATE, + edad INTEGER, + ciudad VARCHAR(100), + activo BOOLEAN + ); + + CREATE TABLE IF NOT EXISTS productos ( + id SERIAL PRIMARY KEY, + nombre VARCHAR(100), + precio DECIMAL(10,2), + categoria VARCHAR(50) + ); + + CREATE TABLE IF NOT EXISTS pedidos ( + id SERIAL PRIMARY KEY, + usuario_id INTEGER REFERENCES usuarios(id), + producto_id INTEGER REFERENCES productos(id), + monto DECIMAL(10,2), + fecha DATE + ); + + INSERT INTO usuarios (nombre, email, fecha_registro, edad, ciudad, activo) VALUES + ('Ana García', 'ana.garcia@email.com', '2023-01-15', 28, 'Madrid', true), + ('Carlos López', 'carlos.lopez@email.com', '2023-02-20', 35, 'Barcelona', true), + ('María Rodríguez', 'maria.rodriguez@email.com', '2023-03-10', 42, 'Valencia', false), + ('Juan Martínez', 'juan.martinez@email.com', '2023-04-05', 31, 'Sevilla', true), + ('Laura Sánchez', 'laura.sanchez@email.com', '2023-05-12', 29, 'Bilbao', true), + ('Pedro Ramírez', 'pedro.ramirez@email.com', '2023-06-18', 38, 'Málaga', false), + ('Sofia Torres', 'sofia.torres@email.com', '2023-07-22', 33, 'Zaragoza', true), + ('Diego Herrera', 'diego.herrera@email.com', '2023-08-30', 45, 'Alicante', true), + ('Carmen Ruiz', 'carmen.ruiz@email.com', '2023-09-14', 27, 'Granada', false), + ('Miguel Flores', 'miguel.flores@email.com', '2023-10-25', 36, 'Murcia', true); + + INSERT INTO productos (nombre, precio, categoria) VALUES + ('Laptop Pro', 1299.99, 'Electrónica'), + ('Smartphone X', 899.50, 'Electrónica'), + ('Auriculares Wireless', 149.99, 'Electrónica'), + ('Camiseta Premium', 45.00, 'Ropa'), + ('Zapatillas Running', 120.00, 'Ropa'), + ('Libro SQL Avanzado', 35.50, 'Libros'), + ('Teclado Mecánico', 89.99, 'Electrónica'), + ('Mochila Viaje', 75.00, 'Accesorios'); + + INSERT INTO pedidos (usuario_id, producto_id, monto, fecha) VALUES + (1, 1, 150.50, '2023-02-01'), + (1, 3, 200.75, '2023-03-15'), + (2, 2, 350.00, '2023-02-28'), + (3, 4, 125.25, '2023-04-10'), + (4, 1, 475.00, '2023-05-05'), + (4, 5, 225.50, '2023-06-20'), + (5, 6, 180.75, '2023-07-12'), + (6, 7, 300.00, '2023-08-18'), + (7, 2, 425.25, '2023-09-22'), + (7, 8, 150.00, '2023-10-05'), + (8, 3, 275.50, '2023-11-15'), + (9, 4, 190.75, '2023-12-01'), + (10, 1, 400.00, '2023-12-10'), + (10, 5, 325.25, '2023-12-20'); + `); + + return db; +} + +/** + * Set up DDL schema for DDL exercises + */ +async function setupDDLSchema(db: PGlite, setupSQL?: string): Promise { + await db.exec(` + DROP SCHEMA IF EXISTS ${DDL_SCHEMA} CASCADE; + CREATE SCHEMA ${DDL_SCHEMA}; + SET search_path TO ${DDL_SCHEMA}, public; + `); + + if (setupSQL) { + await db.exec(setupSQL); + } +} + +describe("DML Exercise Solutions", () => { + let db: PGlite; + + beforeAll(async () => { + db = await createTestDatabase(); + }); + + afterAll(async () => { + await db.close(); + }); + + const dmlExercises = exercisesData.filter((ex) => ex.type !== "ddl"); + const dmlSolutions = SAMPLE_SOLUTIONS.filter((s) => s.type === "dml"); + + it("should have sample solutions for all DML exercises", () => { + const exerciseTitles = dmlExercises.map((ex) => ex.title); + const solutionTitles = dmlSolutions.map((s) => s.exerciseTitle); + + for (const title of exerciseTitles) { + expect( + solutionTitles.includes(title), + `Missing sample solution for DML exercise: "${title}"`, + ).toBe(true); + } + }); + + describe("Execute sample solutions", () => { + it.each( + dmlSolutions.map((s) => [s.exerciseTitle, s.sql]), + )('"%s" executes without errors', async (_title, sql) => { + await expect(db.query(sql)).resolves.not.toThrow(); + }); + }); + + describe("Critical exercises verification", () => { + it("Multiple JOINs exercise returns correct columns", async () => { + const solution = getSolutionForExercise("Múltiples JOINs"); + expect(solution).toBeDefined(); + if (!solution) return; + + const result = await db.query(solution.sql); + const columns = result.fields.map((f) => f.name); + + expect(columns).toContain("nombre"); + expect(columns).toContain("monto"); + expect(columns).toContain("producto"); + }); + + it("UNION exercise returns results from both tables", async () => { + const solution = getSolutionForExercise("UNION - Combinar Resultados"); + expect(solution).toBeDefined(); + if (!solution) return; + + const result = await db.query(solution.sql); + + // Should have names from both usuarios and productos + expect(result.rows.length).toBeGreaterThan(0); + }); + + it("Total de Pedidos por Usuario returns expected columns", async () => { + const solution = getSolutionForExercise("Total de Pedidos por Usuario"); + expect(solution).toBeDefined(); + if (!solution) return; + + const result = await db.query(solution.sql); + const columns = result.fields.map((f) => f.name); + + expect(columns).toContain("nombre"); + expect(columns).toContain("email"); + expect(columns).toContain("total_pedidos"); + }); + }); +}); + +describe("DDL Exercise Solutions", () => { + const ddlExercises = exercisesData.filter((ex) => ex.type === "ddl"); + const ddlSolutions = SAMPLE_SOLUTIONS.filter((s) => s.type === "ddl"); + + it("should have sample solutions for all DDL exercises", () => { + const exerciseTitles = ddlExercises.map((ex) => ex.title); + const solutionTitles = ddlSolutions.map((s) => s.exerciseTitle); + + for (const title of exerciseTitles) { + expect( + solutionTitles.includes(title), + `Missing sample solution for DDL exercise: "${title}"`, + ).toBe(true); + } + }); + + describe("Execute DDL solutions with setup", () => { + it.each( + ddlExercises.map((ex) => { + const solution = getSolutionForExercise(ex.title); + const setupSQL = (ex.validation.conditions as { setupSQL?: string }) + .setupSQL; + return [ex.title, solution?.sql ?? "", setupSQL ?? ""]; + }), + )('"%s" executes without errors', async (_title, sql, setupSQL) => { + if (!sql) { + return; // Skip if no solution + } + + const db = await createTestDatabase(); + try { + await setupDDLSchema(db, setupSQL || undefined); + await expect(db.query(sql)).resolves.not.toThrow(); + } finally { + await db.close(); + } + }); + }); +}); + +describe("Exercise Coverage", () => { + it("all exercises should have a sample solution", () => { + const allExerciseTitles = exercisesData.map((ex) => ex.title); + const allSolutionTitles = SAMPLE_SOLUTIONS.map((s) => s.exerciseTitle); + + const missingExercises = allExerciseTitles.filter( + (title) => !allSolutionTitles.includes(title), + ); + + expect( + missingExercises, + `Missing solutions for: ${missingExercises.join(", ")}`, + ).toHaveLength(0); + }); +}); From fc4a99254cce96c429586d30134338d8714d1d85 Mon Sep 17 00:00:00 2001 From: Cris Date: Sun, 4 Jan 2026 06:21:53 -0500 Subject: [PATCH 5/9] ci: add GitHub Actions workflow for pre-merge checks - Run lint, test, and build on PRs and pushes to main - Use Bun for fast dependency installation - Block merging if any step fails --- .github/workflows/ci.yml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9297794 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + validate: + name: Lint, Test & Build + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Run linter + run: bun run lint + + - name: Run tests + run: bun run test + + - name: Build application + run: bun run build + env: + NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY || 'pk_test_placeholder' }} + CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY || 'sk_test_placeholder' }} + DATABASE_URL: ${{ secrets.DATABASE_URL || 'postgresql://placeholder:placeholder@localhost:5432/placeholder' }} + From c880f4ab59824d1cded781053ebc590285d2676c Mon Sep 17 00:00:00 2001 From: Cris Date: Sun, 4 Jan 2026 06:22:00 -0500 Subject: [PATCH 6/9] style: apply biome formatting to codebase - Convert single quotes to double quotes - Add semicolons to statements - Organize imports alphabetically - Apply consistent formatting across all files --- biome.json | 76 +- drizzle.config.ts | 17 +- drizzle/meta/0000_snapshot.json | 10 +- drizzle/meta/_journal.json | 2 +- next.config.ts | 34 +- scripts/debug-user-submissions.ts | 165 +-- scripts/generate-og.ts | 10 +- scripts/seed-heatmap.ts | 182 +-- src/app/api/ai/hint/route.ts | 48 +- src/app/api/exercises/[id]/route.ts | 32 +- src/app/api/exercises/route.ts | 22 +- src/app/api/leaderboard/route.ts | 68 +- src/app/api/profiles/[userId]/route.ts | 26 +- src/app/api/profiles/sync/route.ts | 83 +- src/app/api/submissions/route.ts | 59 +- src/app/api/users/[userId]/heatmap/route.ts | 46 +- src/app/api/users/[userId]/history/route.ts | 51 +- src/app/api/users/[userId]/score/route.ts | 47 +- .../[userId]/solutions/[exerciseId]/route.ts | 34 +- .../users/[userId]/solved-exercises/route.ts | 39 +- src/app/api/users/[userId]/streak/route.ts | 79 +- .../api/users/[userId]/week-progress/route.ts | 61 +- src/app/docs/layout.tsx | 17 +- src/app/docs/page.tsx | 140 +- src/app/exercises/[id]/page.tsx | 38 +- src/app/exercises/page.tsx | 11 +- src/app/layout.tsx | 179 +-- src/app/leaderboard/page.tsx | 37 +- src/app/page.tsx | 12 +- src/app/profile/page.tsx | 73 +- src/app/providers.tsx | 12 +- src/components/auth/Profile.tsx | 24 +- src/components/auth/index.ts | 7 +- src/components/auth/profile-sync.tsx | 9 +- src/components/auth/user-profile.tsx | 13 +- src/components/docs/index.ts | 5 +- src/components/docs/schema-viewer.tsx | 250 ++-- src/components/docs/sql-reference.tsx | 280 ++-- src/components/exercises/exercise-card.tsx | 87 +- src/components/exercises/exercise-grid.tsx | 95 +- src/components/exercises/exercise-view.tsx | 461 ++++--- src/components/exercises/index.ts | 13 +- src/components/exercises/schema-helper.tsx | 77 +- .../exercises/schema-state-viewer.tsx | 186 +-- src/components/exercises/sql-editor.tsx | 542 +++++--- src/components/landing/index.ts | 5 +- src/components/landing/landing-page.tsx | 149 ++- src/components/landing/sql-demo-animation.tsx | 312 +++-- src/components/layout/header.tsx | 86 +- src/components/layout/index.ts | 5 +- src/components/leaderboard/index.ts | 3 +- .../leaderboard/leaderboard-table.tsx | 104 +- src/components/logos/kebo.tsx | 57 +- src/components/logos/moraleja-design.tsx | 39 +- src/components/profile/activity-heatmap.tsx | 41 +- src/components/profile/exercise-history.tsx | 55 +- src/components/profile/github-heatmap.tsx | 325 +++-- src/components/profile/index.ts | 13 +- src/components/profile/stats-grid.tsx | 41 +- src/components/profile/user-avatar.tsx | 63 +- src/components/shared/ai-hint.tsx | 150 ++- src/components/shared/celebration.tsx | 37 +- src/components/shared/error-boundary.tsx | 39 +- src/components/shared/error-message.tsx | 47 +- src/components/shared/github-badge.tsx | 53 +- src/components/shared/index.ts | 23 +- src/components/shared/results-table.tsx | 43 +- src/components/shared/score-badge.tsx | 39 +- src/components/shared/signup-prompt-modal.tsx | 32 +- src/components/shared/streak-badge.tsx | 39 +- src/components/shared/success-modal.tsx | 41 +- src/components/shared/theme-toggle.tsx | 102 +- src/components/ui/accordion.tsx | 28 +- src/components/ui/avatar.tsx | 35 +- src/components/ui/badge.tsx | 14 +- src/components/ui/button.tsx | 26 +- src/components/ui/card.tsx | 39 +- src/components/ui/dialog.tsx | 54 +- src/components/ui/dropdown-menu.tsx | 82 +- src/components/ui/index.ts | 31 +- src/components/ui/input.tsx | 23 +- src/components/ui/label.tsx | 23 +- src/components/ui/separator.tsx | 20 +- src/components/ui/sheet.tsx | 56 +- src/components/ui/skeleton.tsx | 11 +- src/components/ui/table.tsx | 46 +- src/components/ui/tabs.tsx | 30 +- src/components/ui/tooltip.tsx | 28 +- src/data/ejercicios.ts | 6 +- src/hooks/use-exercises.ts | 47 +- src/hooks/use-leaderboard.ts | 33 +- src/hooks/use-local-query.ts | 85 +- src/hooks/use-profile-sync.ts | 31 +- src/hooks/use-profile.ts | 61 +- src/hooks/use-submissions.ts | 295 ++-- src/lib/db/index.ts | 15 +- src/lib/db/schema.ts | 96 +- src/lib/db/seed-data.ts | 1183 ++++++++++------- src/lib/db/seed.ts | 54 +- src/lib/ddl-validator.ts | 330 +++-- src/lib/exercises-service.ts | 113 +- src/lib/query-client.ts | 35 +- src/lib/query-validator.ts | 117 +- src/lib/sql-error-handler.ts | 42 +- src/lib/utils.ts | 34 +- src/lib/validation-service.ts | 88 +- src/lib/validations/index.ts | 79 +- src/proxy.ts | 13 +- src/stores/index.ts | 3 +- src/stores/theme-store.ts | 17 +- src/types/database.ts | 4 +- src/types/exercises.ts | 60 +- tailwind.config.ts | 127 +- tsconfig.json | 14 +- 114 files changed, 5130 insertions(+), 4100 deletions(-) diff --git a/biome.json b/biome.json index 2db32aa..5688e64 100644 --- a/biome.json +++ b/biome.json @@ -1,40 +1,40 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json", - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true - }, - "files": { - "ignoreUnknown": false - }, - "formatter": { - "enabled": true, - "indentStyle": "space" - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "suspicious": { - "noExplicitAny": "off" - }, - "correctness": { - "noUnusedVariables": "warn" - } - } - }, - "javascript": { - "formatter": { - "quoteStyle": "double" - } - }, - "assist": { - "enabled": true, - "actions": { - "source": { - "organizeImports": "on" - } - } - } + "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false + }, + "formatter": { + "enabled": true, + "indentStyle": "space" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "off" + }, + "correctness": { + "noUnusedVariables": "warn" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } } diff --git a/drizzle.config.ts b/drizzle.config.ts index b10f1f5..02ca889 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,15 +1,14 @@ -import { defineConfig } from 'drizzle-kit' -import 'dotenv/config' -import { config } from 'dotenv' +import { defineConfig } from "drizzle-kit"; +import "dotenv/config"; +import { config } from "dotenv"; -config({ path: '.env.local' }) +config({ path: ".env.local" }); export default defineConfig({ - schema: './src/lib/db/schema.ts', - out: './drizzle', - dialect: 'postgresql', + schema: "./src/lib/db/schema.ts", + out: "./drizzle", + dialect: "postgresql", dbCredentials: { url: process.env.DATABASE_URL!, }, -}) - +}); diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index 08c14a9..f9ced15 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -162,12 +162,8 @@ "name": "submissions_exercise_id_exercises_id_fk", "tableFrom": "submissions", "tableTo": "exercises", - "columnsFrom": [ - "exercise_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["exercise_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -190,4 +186,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 58bae5e..b28ada5 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -24,4 +24,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/next.config.ts b/next.config.ts index 3eaaba5..8fdb586 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,32 +3,32 @@ const nextConfig = { images: { remotePatterns: [ { - protocol: 'https', - hostname: 'www.google.com', - pathname: '/**', + protocol: "https", + hostname: "www.google.com", + pathname: "/**", }, { - protocol: 'https', - hostname: 'lh3.googleusercontent.com', - pathname: '/**', + protocol: "https", + hostname: "lh3.googleusercontent.com", + pathname: "/**", }, { - protocol: 'https', - hostname: 'pglite.dev', - pathname: '/**', + protocol: "https", + hostname: "pglite.dev", + pathname: "/**", }, { - protocol: 'https', - hostname: 'img.clerk.com', - pathname: '/**', + protocol: "https", + hostname: "img.clerk.com", + pathname: "/**", }, { - protocol: 'https', - hostname: '*.clerk.com', - pathname: '/**', + protocol: "https", + hostname: "*.clerk.com", + pathname: "/**", }, ], }, -} +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/scripts/debug-user-submissions.ts b/scripts/debug-user-submissions.ts index 8dbba42..967d8f5 100644 --- a/scripts/debug-user-submissions.ts +++ b/scripts/debug-user-submissions.ts @@ -1,29 +1,30 @@ -import { config } from 'dotenv' -config({ path: '.env.local' }) +import { config } from "dotenv"; -import { neon } from '@neondatabase/serverless' -import { drizzle } from 'drizzle-orm/neon-http' -import { submissions } from '../src/lib/db/schema' -import { eq, desc, gte, and } from 'drizzle-orm' +config({ path: ".env.local" }); -const USER_ID = process.argv[2] || 'user_37PFmZhlxMbK0JtsR2qPHCMnFJK' +import { neon } from "@neondatabase/serverless"; +import { and, desc, eq, gte } from "drizzle-orm"; +import { drizzle } from "drizzle-orm/neon-http"; +import { submissions } from "../src/lib/db/schema"; + +const USER_ID = process.argv[2] || "user_37PFmZhlxMbK0JtsR2qPHCMnFJK"; async function debugUserSubmissions() { - const databaseUrl = process.env.DATABASE_URL + const databaseUrl = process.env.DATABASE_URL; if (!databaseUrl) { - console.error('DATABASE_URL environment variable is not set') - process.exit(1) + console.error("DATABASE_URL environment variable is not set"); + process.exit(1); } - console.log('='.repeat(60)) - console.log('DEBUG: User Submissions') - console.log('='.repeat(60)) - console.log('User ID:', USER_ID) - console.log('') + console.log("=".repeat(60)); + console.log("DEBUG: User Submissions"); + console.log("=".repeat(60)); + console.log("User ID:", USER_ID); + console.log(""); - const sql = neon(databaseUrl) - const db = drizzle(sql) + const sql = neon(databaseUrl); + const db = drizzle(sql); try { // Get all submissions for the user @@ -31,100 +32,110 @@ async function debugUserSubmissions() { .select() .from(submissions) .where(eq(submissions.userId, USER_ID)) - .orderBy(desc(submissions.createdAt)) + .orderBy(desc(submissions.createdAt)); - console.log(`Total submissions found: ${allSubmissions.length}`) - console.log('') + console.log(`Total submissions found: ${allSubmissions.length}`); + console.log(""); if (allSubmissions.length === 0) { - console.log('No submissions found for this user.') - return + console.log("No submissions found for this user."); + return; } // Show last 10 submissions - console.log('Last 10 submissions:') - console.log('-'.repeat(60)) - - const recent = allSubmissions.slice(0, 10) + console.log("Last 10 submissions:"); + console.log("-".repeat(60)); + + const recent = allSubmissions.slice(0, 10); for (const sub of recent) { - console.log(` ID: ${sub.id}`) - console.log(` Created At: ${sub.createdAt.toISOString()}`) - console.log(` Local Time: ${sub.createdAt.toLocaleString()}`) - console.log(` Exercise ID: ${sub.exerciseId}`) - console.log(` Score: ${sub.score}`) - console.log('-'.repeat(60)) + console.log(` ID: ${sub.id}`); + console.log(` Created At: ${sub.createdAt.toISOString()}`); + console.log(` Local Time: ${sub.createdAt.toLocaleString()}`); + console.log(` Exercise ID: ${sub.exerciseId}`); + console.log(` Score: ${sub.score}`); + console.log("-".repeat(60)); } // Check submissions for current year (heatmap logic) - const currentYear = new Date().getFullYear() - const startOfYear = new Date(currentYear, 0, 1) - - console.log('') - console.log('Heatmap Query Check:') - console.log(` Current Year: ${currentYear}`) - console.log(` Start of Year: ${startOfYear.toISOString()}`) - + const currentYear = new Date().getFullYear(); + const startOfYear = new Date(currentYear, 0, 1); + + console.log(""); + console.log("Heatmap Query Check:"); + console.log(` Current Year: ${currentYear}`); + console.log(` Start of Year: ${startOfYear.toISOString()}`); + const heatmapSubmissions = await db .select({ createdAt: submissions.createdAt }) .from(submissions) .where( and( eq(submissions.userId, USER_ID), - gte(submissions.createdAt, startOfYear) - ) - ) + gte(submissions.createdAt, startOfYear), + ), + ); - console.log(` Submissions in ${currentYear}: ${heatmapSubmissions.length}`) - console.log('') + console.log( + ` Submissions in ${currentYear}: ${heatmapSubmissions.length}`, + ); + console.log(""); // Check today's submissions - const today = new Date() - const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()) - const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1) - - console.log('Today Check:') - console.log(` Today (UTC): ${today.toISOString()}`) - console.log(` Start of Today: ${startOfToday.toISOString()}`) - console.log(` End of Today: ${endOfToday.toISOString()}`) - - const todaySubmissions = allSubmissions.filter(sub => { - const subDate = new Date(sub.createdAt) - return subDate >= startOfToday && subDate < endOfToday - }) - - console.log(` Submissions today: ${todaySubmissions.length}`) - + const today = new Date(); + const startOfToday = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate(), + ); + const endOfToday = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate() + 1, + ); + + console.log("Today Check:"); + console.log(` Today (UTC): ${today.toISOString()}`); + console.log(` Start of Today: ${startOfToday.toISOString()}`); + console.log(` End of Today: ${endOfToday.toISOString()}`); + + const todaySubmissions = allSubmissions.filter((sub) => { + const subDate = new Date(sub.createdAt); + return subDate >= startOfToday && subDate < endOfToday; + }); + + console.log(` Submissions today: ${todaySubmissions.length}`); + if (todaySubmissions.length > 0) { - console.log('') - console.log('Today\'s submissions:') + console.log(""); + console.log("Today's submissions:"); for (const sub of todaySubmissions) { - console.log(` - ${sub.createdAt.toISOString()} | Exercise: ${sub.exerciseId} | Score: ${sub.score}`) + console.log( + ` - ${sub.createdAt.toISOString()} | Exercise: ${sub.exerciseId} | Score: ${sub.score}`, + ); } } // Group by date for heatmap visualization - console.log('') - console.log('Submissions by date (last 7 days):') - const dateCounts: Record = {} - + console.log(""); + console.log("Submissions by date (last 7 days):"); + const dateCounts: Record = {}; + for (const sub of allSubmissions) { - const dateKey = sub.createdAt.toISOString().split('T')[0] - dateCounts[dateKey] = (dateCounts[dateKey] || 0) + 1 + const dateKey = sub.createdAt.toISOString().split("T")[0]; + dateCounts[dateKey] = (dateCounts[dateKey] || 0) + 1; } const sortedDates = Object.entries(dateCounts) .sort((a, b) => b[0].localeCompare(a[0])) - .slice(0, 7) + .slice(0, 7); for (const [date, count] of sortedDates) { - console.log(` ${date}: ${count} submissions`) + console.log(` ${date}: ${count} submissions`); } - } catch (error) { - console.error('Error:', error) - process.exit(1) + console.error("Error:", error); + process.exit(1); } } -debugUserSubmissions() - +debugUserSubmissions(); diff --git a/scripts/generate-og.ts b/scripts/generate-og.ts index cfe7579..449d63c 100644 --- a/scripts/generate-og.ts +++ b/scripts/generate-og.ts @@ -1,7 +1,7 @@ -import satori from "satori"; import { Resvg } from "@resvg/resvg-js"; import { writeFileSync } from "fs"; import { join } from "path"; +import satori from "satori"; const WIDTH = 1200; const HEIGHT = 630; @@ -19,13 +19,13 @@ const colors = { async function generateOG() { // Fetch Inter font (regular weight) const interRes = await fetch( - "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_EeA.woff" + "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_EeA.woff", ); const interData = await interRes.arrayBuffer(); // Fetch Inter font (semibold weight) const interSemiboldRes = await fetch( - "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuGKYAZ9hjp-Ek-_EeA.woff" + "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuGKYAZ9hjp-Ek-_EeA.woff", ); const interSemiboldData = await interSemiboldRes.arrayBuffer(); @@ -192,7 +192,7 @@ async function generateOG() { style: "normal", }, ], - } + }, ); const resvg = new Resvg(svg, { @@ -281,7 +281,7 @@ async function generateFavicon() { width: size, height: size, fonts: [], - } + }, ); const resvg = new Resvg(svg, { diff --git a/scripts/seed-heatmap.ts b/scripts/seed-heatmap.ts index 5ffb5d1..90f0f82 100644 --- a/scripts/seed-heatmap.ts +++ b/scripts/seed-heatmap.ts @@ -1,50 +1,53 @@ -import { config } from 'dotenv' -config({ path: '.env.local' }) +import { config } from "dotenv"; -import { neon } from '@neondatabase/serverless' -import { drizzle } from 'drizzle-orm/neon-http' -import { submissions, exercises } from '../src/lib/db/schema' +config({ path: ".env.local" }); -const USER_ID = 'user_37N3N55IddhoQXNefDjKozEmAZ6' +import { neon } from "@neondatabase/serverless"; +import { drizzle } from "drizzle-orm/neon-http"; +import { exercises, submissions } from "../src/lib/db/schema"; + +const USER_ID = "user_37N3N55IddhoQXNefDjKozEmAZ6"; async function seedHeatmapData() { - const databaseUrl = process.env.DATABASE_URL + const databaseUrl = process.env.DATABASE_URL; if (!databaseUrl) { - console.error('❌ DATABASE_URL environment variable is not set') - console.error(' Make sure you have a .env.local file with DATABASE_URL') - process.exit(1) + console.error("❌ DATABASE_URL environment variable is not set"); + console.error(" Make sure you have a .env.local file with DATABASE_URL"); + process.exit(1); } - console.log('🌱 Seeding heatmap data for user:', USER_ID) + console.log("🌱 Seeding heatmap data for user:", USER_ID); - const sql = neon(databaseUrl) - const db = drizzle(sql) + const sql = neon(databaseUrl); + const db = drizzle(sql); try { // Get existing exercises - const existingExercises = await db.select({ id: exercises.id }).from(exercises) - + const existingExercises = await db + .select({ id: exercises.id }) + .from(exercises); + if (existingExercises.length === 0) { - console.error('❌ No exercises found. Run db:seed first.') - process.exit(1) + console.error("❌ No exercises found. Run db:seed first."); + process.exit(1); } - console.log(`📚 Found ${existingExercises.length} exercises`) - const exerciseIds = existingExercises.map(e => e.id) + console.log(`📚 Found ${existingExercises.length} exercises`); + const exerciseIds = existingExercises.map((e) => e.id); // Generate random submissions across 2025 const submissionsToInsert: { - userId: string - exerciseId: string - score: number - solution: string - attempts: number - createdAt: Date - }[] = [] - - const year = 2025 - + userId: string; + exerciseId: string; + score: number; + solution: string; + attempts: number; + createdAt: Date; + }[] = []; + + const year = 2025; + // Create varied activity patterns const activityPatterns = [ // January - moderate activity @@ -71,96 +74,125 @@ async function seedHeatmapData() { { month: 10, daysActive: 16, maxPerDay: 3 }, // December (up to today) - high activity streak { month: 11, daysActive: 20, maxPerDay: 4 }, - ] + ]; - const today = new Date() + const today = new Date(); for (const pattern of activityPatterns) { - const daysInMonth = new Date(year, pattern.month + 1, 0).getDate() - const activeDays = new Set() + const daysInMonth = new Date(year, pattern.month + 1, 0).getDate(); + const activeDays = new Set(); // Pick random days to be active - while (activeDays.size < pattern.daysActive && activeDays.size < daysInMonth) { - const day = Math.floor(Math.random() * daysInMonth) + 1 - const date = new Date(year, pattern.month, day) - + while ( + activeDays.size < pattern.daysActive && + activeDays.size < daysInMonth + ) { + const day = Math.floor(Math.random() * daysInMonth) + 1; + const date = new Date(year, pattern.month, day); + // Don't add future dates if (date <= today) { - activeDays.add(day) + activeDays.add(day); } } // Create submissions for each active day for (const day of activeDays) { - const submissionsCount = Math.floor(Math.random() * pattern.maxPerDay) + 1 - + const submissionsCount = + Math.floor(Math.random() * pattern.maxPerDay) + 1; + for (let i = 0; i < submissionsCount; i++) { - const exerciseId = exerciseIds[Math.floor(Math.random() * exerciseIds.length)] - const hour = Math.floor(Math.random() * 14) + 8 // 8am - 10pm - const minute = Math.floor(Math.random() * 60) - - const createdAt = new Date(year, pattern.month, day, hour, minute) - + const exerciseId = + exerciseIds[Math.floor(Math.random() * exerciseIds.length)]; + const hour = Math.floor(Math.random() * 14) + 8; // 8am - 10pm + const minute = Math.floor(Math.random() * 60); + + const createdAt = new Date(year, pattern.month, day, hour, minute); + submissionsToInsert.push({ userId: USER_ID, exerciseId, score: Math.random() > 0.3 ? 2 : 1, // 70% get full score - solution: 'SELECT * FROM seeded_data;', + solution: "SELECT * FROM seeded_data;", attempts: Math.floor(Math.random() * 3) + 1, createdAt, - }) + }); } } } // Add a streak for the last 7 days for (let i = 0; i < 7; i++) { - const date = new Date(today) - date.setDate(date.getDate() - i) - + const date = new Date(today); + date.setDate(date.getDate() - i); + if (date.getFullYear() === year) { - const exerciseId = exerciseIds[Math.floor(Math.random() * exerciseIds.length)] + const exerciseId = + exerciseIds[Math.floor(Math.random() * exerciseIds.length)]; submissionsToInsert.push({ userId: USER_ID, exerciseId, score: 2, - solution: 'SELECT * FROM streak_data;', + solution: "SELECT * FROM streak_data;", attempts: 1, - createdAt: new Date(date.getFullYear(), date.getMonth(), date.getDate(), 14, 30), - }) + createdAt: new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + 14, + 30, + ), + }); } } - console.log(`📝 Inserting ${submissionsToInsert.length} submissions...`) + console.log(`📝 Inserting ${submissionsToInsert.length} submissions...`); // Insert in batches - const batchSize = 50 + const batchSize = 50; for (let i = 0; i < submissionsToInsert.length; i += batchSize) { - const batch = submissionsToInsert.slice(i, i + batchSize) - await db.insert(submissions).values(batch) - console.log(` ✓ Inserted batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(submissionsToInsert.length / batchSize)}`) + const batch = submissionsToInsert.slice(i, i + batchSize); + await db.insert(submissions).values(batch); + console.log( + ` ✓ Inserted batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(submissionsToInsert.length / batchSize)}`, + ); } - console.log(`\n✅ Successfully seeded ${submissionsToInsert.length} submissions for heatmap!`) - + console.log( + `\n✅ Successfully seeded ${submissionsToInsert.length} submissions for heatmap!`, + ); + // Summary by month - const monthCounts: Record = {} + const monthCounts: Record = {}; for (const sub of submissionsToInsert) { - const month = sub.createdAt.getMonth() - monthCounts[month] = (monthCounts[month] || 0) + 1 - } - - console.log('\n📊 Summary by month:') - const monthNames = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'] - for (const [month, count] of Object.entries(monthCounts).sort((a, b) => Number(a[0]) - Number(b[0]))) { - console.log(` ${monthNames[Number(month)]}: ${count} submissions`) + const month = sub.createdAt.getMonth(); + monthCounts[month] = (monthCounts[month] || 0) + 1; } + console.log("\n📊 Summary by month:"); + const monthNames = [ + "Ene", + "Feb", + "Mar", + "Abr", + "May", + "Jun", + "Jul", + "Ago", + "Sep", + "Oct", + "Nov", + "Dic", + ]; + for (const [month, count] of Object.entries(monthCounts).sort( + (a, b) => Number(a[0]) - Number(b[0]), + )) { + console.log(` ${monthNames[Number(month)]}: ${count} submissions`); + } } catch (error) { - console.error('❌ Error seeding heatmap data:', error) - process.exit(1) + console.error("❌ Error seeding heatmap data:", error); + process.exit(1); } } -seedHeatmapData() - +seedHeatmapData(); diff --git a/src/app/api/ai/hint/route.ts b/src/app/api/ai/hint/route.ts index 4ea4f1f..1b43e3e 100644 --- a/src/app/api/ai/hint/route.ts +++ b/src/app/api/ai/hint/route.ts @@ -1,8 +1,8 @@ -import { createGroq } from '@ai-sdk/groq' -import { streamText } from 'ai' -import { z } from 'zod' +import { createGroq } from "@ai-sdk/groq"; +import { streamText } from "ai"; +import { z } from "zod"; -const groq = createGroq() +const groq = createGroq(); const hintRequestSchema = z.object({ exercise: z.object({ @@ -10,13 +10,13 @@ const hintRequestSchema = z.object({ description: z.string(), details: z.string(), hint: z.string(), - type: z.enum(['dml', 'ddl']).optional(), + type: z.enum(["dml", "ddl"]).optional(), }), userQuery: z.string(), error: z.string(), schema: z.string().optional(), previousHint: z.string().optional(), -}) +}); const SYSTEM_PROMPT = `You are a SQL tutor helping students learn SQL through practice. Your role is to guide, not solve. @@ -31,21 +31,22 @@ Guidelines: - Be encouraging and supportive - Respond in Spanish since the platform is in Spanish -Remember: The goal is learning, not just getting the right answer.` +Remember: The goal is learning, not just getting the right answer.`; export async function POST(req: Request) { try { - const body = await req.json() - const { exercise, userQuery, error, schema, previousHint } = hintRequestSchema.parse(body) + const body = await req.json(); + const { exercise, userQuery, error, schema, previousHint } = + hintRequestSchema.parse(body); - const isFollowUp = !!previousHint + const isFollowUp = !!previousHint; const prompt = isFollowUp ? ` Exercise: ${exercise.title} Description: ${exercise.description} Details: ${exercise.details} -Exercise type: ${exercise.type || 'dml'} +Exercise type: ${exercise.type || "dml"} User's SQL query: \`\`\`sql @@ -54,7 +55,7 @@ ${userQuery} Error message: ${error} -${schema ? `Available schema:\n${schema}` : ''} +${schema ? `Available schema:\n${schema}` : ""} Previous hint given: "${previousHint}" @@ -68,7 +69,7 @@ The student didn't understand the previous hint and needs a clearer explanation. Exercise: ${exercise.title} Description: ${exercise.description} Details: ${exercise.details} -Exercise type: ${exercise.type || 'dml'} +Exercise type: ${exercise.type || "dml"} User's SQL query: \`\`\`sql @@ -77,12 +78,12 @@ ${userQuery} Error message: ${error} -${schema ? `Available schema:\n${schema}` : ''} +${schema ? `Available schema:\n${schema}` : ""} -Provide a brief, educational hint to help the student understand their mistake without giving away the answer.` +Provide a brief, educational hint to help the student understand their mistake without giving away the answer.`; const result = streamText({ - model: groq('openai/gpt-oss-20b'), + model: groq("openai/gpt-oss-20b"), system: SYSTEM_PROMPT, prompt, providerOptions: { @@ -91,15 +92,14 @@ Provide a brief, educational hint to help the student understand their mistake w temperature: 0.7, }, }, - }) + }); - return result.toTextStreamResponse() + return result.toTextStreamResponse(); } catch (err) { - console.error('AI hint error:', err) - return new Response( - JSON.stringify({ error: 'Failed to generate hint' }), - { status: 500, headers: { 'Content-Type': 'application/json' } } - ) + console.error("AI hint error:", err); + return new Response(JSON.stringify({ error: "Failed to generate hint" }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); } } - diff --git a/src/app/api/exercises/[id]/route.ts b/src/app/api/exercises/[id]/route.ts index 9c51a52..6f0a9cf 100644 --- a/src/app/api/exercises/[id]/route.ts +++ b/src/app/api/exercises/[id]/route.ts @@ -1,26 +1,26 @@ -import { NextRequest, NextResponse } from 'next/server' -import { db } from '@/lib/db' -import { exercises } from '@/lib/db/schema' -import { eq, and } from 'drizzle-orm' +import { and, eq } from "drizzle-orm"; +import { type NextRequest, NextResponse } from "next/server"; +import { db } from "@/lib/db"; +import { exercises } from "@/lib/db/schema"; export async function GET( _request: NextRequest, - { params }: { params: Promise<{ id: string }> } + { params }: { params: Promise<{ id: string }> }, ) { - const { id } = await params + const { id } = await params; try { const [exercise] = await db .select() .from(exercises) .where(and(eq(exercises.id, id), eq(exercises.isDeleted, false))) - .limit(1) + .limit(1); if (!exercise) { return NextResponse.json( - { error: 'Exercise not found' }, - { status: 404 } - ) + { error: "Exercise not found" }, + { status: 404 }, + ); } return NextResponse.json({ @@ -38,14 +38,12 @@ export async function GET( createdAt: exercise.createdAt, updatedAt: exercise.updatedAt, }, - }) + }); } catch (error) { - console.error('Error fetching exercise:', error) + console.error("Error fetching exercise:", error); return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ) + { error: "Internal server error" }, + { status: 500 }, + ); } } - - diff --git a/src/app/api/exercises/route.ts b/src/app/api/exercises/route.ts index 5c3c92e..125c259 100644 --- a/src/app/api/exercises/route.ts +++ b/src/app/api/exercises/route.ts @@ -1,7 +1,7 @@ -import { NextResponse } from 'next/server' -import { db } from '@/lib/db' -import { exercises } from '@/lib/db/schema' -import { eq } from 'drizzle-orm' +import { eq } from "drizzle-orm"; +import { NextResponse } from "next/server"; +import { db } from "@/lib/db"; +import { exercises } from "@/lib/db/schema"; export async function GET() { try { @@ -9,7 +9,7 @@ export async function GET() { .select() .from(exercises) .where(eq(exercises.isDeleted, false)) - .orderBy(exercises.createdAt) + .orderBy(exercises.createdAt); const formattedExercises = data.map((ex) => ({ id: ex.id, @@ -24,14 +24,14 @@ export async function GET() { validation: ex.validation, createdAt: ex.createdAt, updatedAt: ex.updatedAt, - })) + })); - return NextResponse.json({ exercises: formattedExercises }) + return NextResponse.json({ exercises: formattedExercises }); } catch (error) { - console.error('Error fetching exercises:', error) + console.error("Error fetching exercises:", error); return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ) + { error: "Internal server error" }, + { status: 500 }, + ); } } diff --git a/src/app/api/leaderboard/route.ts b/src/app/api/leaderboard/route.ts index efd48be..87c9353 100644 --- a/src/app/api/leaderboard/route.ts +++ b/src/app/api/leaderboard/route.ts @@ -1,19 +1,19 @@ -import { NextResponse } from 'next/server' -import { db } from '@/lib/db' -import { submissions, profiles } from '@/lib/db/schema' -import { sql, desc, eq } from 'drizzle-orm' +import { desc, eq, sql } from "drizzle-orm"; +import { NextResponse } from "next/server"; +import { db } from "@/lib/db"; +import { profiles, submissions } from "@/lib/db/schema"; -export const dynamic = 'force-dynamic' -export const revalidate = 60 // Revalidate every 60 seconds +export const dynamic = "force-dynamic"; +export const revalidate = 60; // Revalidate every 60 seconds interface LeaderboardEntry { - userId: string - displayName: string - imageUrl: string | null - countryCode: string | null - totalScore: number - exercisesSolved: number - rank: number + userId: string; + displayName: string; + imageUrl: string | null; + countryCode: string | null; + totalScore: number; + exercisesSolved: number; + rank: number; } export async function GET() { @@ -30,25 +30,35 @@ export async function GET() { }) .from(submissions) .leftJoin(profiles, eq(submissions.userId, profiles.id)) - .groupBy(submissions.userId, profiles.displayName, profiles.imageUrl, profiles.countryCode) - .orderBy(desc(sql`SUM(${submissions.score})`)) + .groupBy( + submissions.userId, + profiles.displayName, + profiles.imageUrl, + profiles.countryCode, + ) + .orderBy(desc(sql`SUM(${submissions.score})`)); - const rankedLeaderboard: LeaderboardEntry[] = leaderboard.map((entry, index) => ({ - userId: entry.userId, - displayName: entry.displayName || `User ${entry.userId.slice(0, 6)}`, - imageUrl: entry.imageUrl, - countryCode: entry.countryCode, - totalScore: entry.totalScore, - exercisesSolved: entry.exercisesSolved, - rank: index + 1, - })) + const rankedLeaderboard: LeaderboardEntry[] = leaderboard.map( + (entry, index) => ({ + userId: entry.userId, + displayName: entry.displayName || `User ${entry.userId.slice(0, 6)}`, + imageUrl: entry.imageUrl, + countryCode: entry.countryCode, + totalScore: entry.totalScore, + exercisesSolved: entry.exercisesSolved, + rank: index + 1, + }), + ); - return NextResponse.json({ + return NextResponse.json({ leaderboard: rankedLeaderboard, - updatedAt: new Date().toISOString() - }) + updatedAt: new Date().toISOString(), + }); } catch (error) { - console.error('Error fetching leaderboard:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + console.error("Error fetching leaderboard:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } diff --git a/src/app/api/profiles/[userId]/route.ts b/src/app/api/profiles/[userId]/route.ts index 1c729ba..e2f15db 100644 --- a/src/app/api/profiles/[userId]/route.ts +++ b/src/app/api/profiles/[userId]/route.ts @@ -1,15 +1,15 @@ -import { NextRequest, NextResponse } from 'next/server' -import { db } from '@/lib/db' -import { profiles } from '@/lib/db/schema' -import { eq } from 'drizzle-orm' +import { eq } from "drizzle-orm"; +import { type NextRequest, NextResponse } from "next/server"; +import { db } from "@/lib/db"; +import { profiles } from "@/lib/db/schema"; // GET a specific user's public profile export async function GET( _request: NextRequest, - { params }: { params: Promise<{ userId: string }> } + { params }: { params: Promise<{ userId: string }> }, ) { try { - const { userId } = await params + const { userId } = await params; const [profile] = await db .select({ @@ -20,16 +20,18 @@ export async function GET( }) .from(profiles) .where(eq(profiles.id, userId)) - .limit(1) + .limit(1); if (!profile) { - return NextResponse.json({ profile: null }) + return NextResponse.json({ profile: null }); } - return NextResponse.json({ profile }) + return NextResponse.json({ profile }); } catch (error) { - console.error('Error fetching profile:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + console.error("Error fetching profile:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } - diff --git a/src/app/api/profiles/sync/route.ts b/src/app/api/profiles/sync/route.ts index b4d4ab8..1be3aa4 100644 --- a/src/app/api/profiles/sync/route.ts +++ b/src/app/api/profiles/sync/route.ts @@ -1,46 +1,47 @@ -import { NextResponse } from 'next/server' -import { auth, currentUser } from '@clerk/nextjs/server' -import { db } from '@/lib/db' -import { profiles } from '@/lib/db/schema' -import { eq } from 'drizzle-orm' +import { auth, currentUser } from "@clerk/nextjs/server"; +import { eq } from "drizzle-orm"; +import { NextResponse } from "next/server"; +import { db } from "@/lib/db"; +import { profiles } from "@/lib/db/schema"; export async function POST(request: Request) { try { - const { userId } = await auth() - + const { userId } = await auth(); + if (!userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const user = await currentUser() - + const user = await currentUser(); + if (!user) { - return NextResponse.json({ error: 'User not found' }, { status: 404 }) + return NextResponse.json({ error: "User not found" }, { status: 404 }); } // Get country code from request body (optional) - let countryCode: string | null = null + let countryCode: string | null = null; try { - const body = await request.json() - countryCode = body.countryCode || null + const body = await request.json(); + countryCode = body.countryCode || null; } catch { // No body provided, that's fine } // Build display name - const firstName = user.firstName || '' - const lastName = user.lastName || '' - const displayName = `${firstName} ${lastName}`.trim() || - user.username || - user.emailAddresses[0]?.emailAddress?.split('@')[0] || - 'Usuario' + const firstName = user.firstName || ""; + const lastName = user.lastName || ""; + const displayName = + `${firstName} ${lastName}`.trim() || + user.username || + user.emailAddresses[0]?.emailAddress?.split("@")[0] || + "Usuario"; // Upsert profile const existingProfile = await db .select() .from(profiles) .where(eq(profiles.id, userId)) - .limit(1) + .limit(1); const profileData = { displayName, @@ -48,21 +49,18 @@ export async function POST(request: Request) { imageUrl: user.imageUrl || null, updatedAt: new Date(), ...(countryCode && { countryCode }), - } + }; if (existingProfile.length > 0) { // Update existing profile - await db - .update(profiles) - .set(profileData) - .where(eq(profiles.id, userId)) + await db.update(profiles).set(profileData).where(eq(profiles.id, userId)); } else { // Create new profile await db.insert(profiles).values({ id: userId, ...profileData, createdAt: new Date(), - }) + }); } // Return updated profile @@ -70,38 +68,43 @@ export async function POST(request: Request) { .select() .from(profiles) .where(eq(profiles.id, userId)) - .limit(1) + .limit(1); - return NextResponse.json({ profile: updatedProfile }) + return NextResponse.json({ profile: updatedProfile }); } catch (error) { - console.error('Error syncing profile:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + console.error("Error syncing profile:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } // GET current user's profile export async function GET() { try { - const { userId } = await auth() - + const { userId } = await auth(); + if (!userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const [profile] = await db .select() .from(profiles) .where(eq(profiles.id, userId)) - .limit(1) + .limit(1); if (!profile) { - return NextResponse.json({ profile: null }) + return NextResponse.json({ profile: null }); } - return NextResponse.json({ profile }) + return NextResponse.json({ profile }); } catch (error) { - console.error('Error fetching profile:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + console.error("Error fetching profile:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } - diff --git a/src/app/api/submissions/route.ts b/src/app/api/submissions/route.ts index c9c500f..82578c5 100644 --- a/src/app/api/submissions/route.ts +++ b/src/app/api/submissions/route.ts @@ -1,27 +1,27 @@ -import { NextRequest, NextResponse } from 'next/server' -import { auth } from '@clerk/nextjs/server' -import { db } from '@/lib/db' -import { submissions, exercises } from '@/lib/db/schema' -import { createSubmissionSchema } from '@/lib/validations' -import { eq } from 'drizzle-orm' +import { auth } from "@clerk/nextjs/server"; +import { eq } from "drizzle-orm"; +import { type NextRequest, NextResponse } from "next/server"; +import { db } from "@/lib/db"; +import { exercises, submissions } from "@/lib/db/schema"; +import { createSubmissionSchema } from "@/lib/validations"; export async function POST(request: NextRequest) { - const { userId } = await auth() + const { userId } = await auth(); if (!userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } try { - const body = await request.json() - const parsed = createSubmissionSchema.safeParse(body) + const body = await request.json(); + const parsed = createSubmissionSchema.safeParse(body); if (!parsed.success) { - console.error('Validation failed:', parsed.error.flatten()) + console.error("Validation failed:", parsed.error.flatten()); return NextResponse.json( - { error: 'Invalid request body', details: parsed.error.flatten() }, - { status: 400 } - ) + { error: "Invalid request body", details: parsed.error.flatten() }, + { status: 400 }, + ); } // Verify exercise exists @@ -29,14 +29,14 @@ export async function POST(request: NextRequest) { .select({ id: exercises.id }) .from(exercises) .where(eq(exercises.id, parsed.data.exerciseId)) - .limit(1) + .limit(1); if (!exercise) { - console.error('Exercise not found:', parsed.data.exerciseId) + console.error("Exercise not found:", parsed.data.exerciseId); return NextResponse.json( - { error: 'Exercise not found', exerciseId: parsed.data.exerciseId }, - { status: 404 } - ) + { error: "Exercise not found", exerciseId: parsed.data.exerciseId }, + { status: 404 }, + ); } const [submission] = await db @@ -47,14 +47,21 @@ export async function POST(request: NextRequest) { solution: parsed.data.solution, score: 2, }) - .returning() + .returning(); - console.log('Submission created:', { id: submission.id, exerciseId: submission.exerciseId, userId }) - - return NextResponse.json({ submission }) + console.log("Submission created:", { + id: submission.id, + exerciseId: submission.exerciseId, + userId, + }); + + return NextResponse.json({ submission }); } catch (error) { - console.error('Error creating submission:', error) - const message = error instanceof Error ? error.message : 'Unknown error' - return NextResponse.json({ error: 'Internal server error', details: message }, { status: 500 }) + console.error("Error creating submission:", error); + const message = error instanceof Error ? error.message : "Unknown error"; + return NextResponse.json( + { error: "Internal server error", details: message }, + { status: 500 }, + ); } } diff --git a/src/app/api/users/[userId]/heatmap/route.ts b/src/app/api/users/[userId]/heatmap/route.ts index 6e60adb..ccff660 100644 --- a/src/app/api/users/[userId]/heatmap/route.ts +++ b/src/app/api/users/[userId]/heatmap/route.ts @@ -1,42 +1,44 @@ -import { NextRequest, NextResponse } from 'next/server' -import { auth } from '@clerk/nextjs/server' -import { db } from '@/lib/db' -import { submissions } from '@/lib/db/schema' -import { eq, gte, and } from 'drizzle-orm' +import { auth } from "@clerk/nextjs/server"; +import { and, eq, gte } from "drizzle-orm"; +import { type NextRequest, NextResponse } from "next/server"; +import { db } from "@/lib/db"; +import { submissions } from "@/lib/db/schema"; export async function GET( _request: NextRequest, - { params }: { params: Promise<{ userId: string }> } + { params }: { params: Promise<{ userId: string }> }, ) { - const { userId: authUserId } = await auth() - const { userId } = await params + const { userId: authUserId } = await auth(); + const { userId } = await params; if (!authUserId || authUserId !== userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } try { - const currentYear = new Date().getFullYear() - const startOfYear = new Date(currentYear, 0, 1) - + const currentYear = new Date().getFullYear(); + const startOfYear = new Date(currentYear, 0, 1); + const userSubmissions = await db .select({ createdAt: submissions.createdAt }) .from(submissions) .where( and( eq(submissions.userId, userId), - gte(submissions.createdAt, startOfYear) - ) - ) + gte(submissions.createdAt, startOfYear), + ), + ); - const submissionDates = userSubmissions.map((sub) => - sub.createdAt.toISOString() - ) + const submissionDates = userSubmissions.map((sub) => + sub.createdAt.toISOString(), + ); - return NextResponse.json({ dates: submissionDates }) + return NextResponse.json({ dates: submissionDates }); } catch (error) { - console.error('Error fetching heatmap data:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + console.error("Error fetching heatmap data:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } - diff --git a/src/app/api/users/[userId]/history/route.ts b/src/app/api/users/[userId]/history/route.ts index 0173cbd..4579b05 100644 --- a/src/app/api/users/[userId]/history/route.ts +++ b/src/app/api/users/[userId]/history/route.ts @@ -1,18 +1,18 @@ -import { NextRequest, NextResponse } from 'next/server' -import { auth } from '@clerk/nextjs/server' -import { db } from '@/lib/db' -import { submissions, exercises } from '@/lib/db/schema' -import { eq, desc } from 'drizzle-orm' +import { auth } from "@clerk/nextjs/server"; +import { desc, eq } from "drizzle-orm"; +import { type NextRequest, NextResponse } from "next/server"; +import { db } from "@/lib/db"; +import { exercises, submissions } from "@/lib/db/schema"; export async function GET( _request: NextRequest, - { params }: { params: Promise<{ userId: string }> } + { params }: { params: Promise<{ userId: string }> }, ) { - const { userId: authUserId } = await auth() - const { userId } = await params + const { userId: authUserId } = await auth(); + const { userId } = await params; if (!authUserId || authUserId !== userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } try { @@ -27,27 +27,32 @@ export async function GET( .from(submissions) .innerJoin(exercises, eq(submissions.exerciseId, exercises.id)) .where(eq(submissions.userId, userId)) - .orderBy(desc(submissions.createdAt)) + .orderBy(desc(submissions.createdAt)); // Remove duplicates, keeping only the most recent submission per exercise - const uniqueHistory = userHistory.reduce((acc, item) => { - if (!acc.find((x) => x.exerciseId === item.exerciseId)) { - acc.push(item) - } - return acc - }, []) + const uniqueHistory = userHistory.reduce( + (acc, item) => { + if (!acc.find((x) => x.exerciseId === item.exerciseId)) { + acc.push(item); + } + return acc; + }, + [], + ); - console.log('[History API] User history:', { + console.log("[History API] User history:", { userId, totalSubmissions: userHistory.length, uniqueHistory: uniqueHistory.length, - sample: uniqueHistory[0] - }) + sample: uniqueHistory[0], + }); - return NextResponse.json({ history: uniqueHistory }) + return NextResponse.json({ history: uniqueHistory }); } catch (error) { - console.error('Error fetching history:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + console.error("Error fetching history:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } - diff --git a/src/app/api/users/[userId]/score/route.ts b/src/app/api/users/[userId]/score/route.ts index 447a9ea..928b1c5 100644 --- a/src/app/api/users/[userId]/score/route.ts +++ b/src/app/api/users/[userId]/score/route.ts @@ -1,18 +1,18 @@ -import { NextRequest, NextResponse } from 'next/server' -import { auth } from '@clerk/nextjs/server' -import { db } from '@/lib/db' -import { submissions } from '@/lib/db/schema' -import { eq } from 'drizzle-orm' +import { auth } from "@clerk/nextjs/server"; +import { eq } from "drizzle-orm"; +import { type NextRequest, NextResponse } from "next/server"; +import { db } from "@/lib/db"; +import { submissions } from "@/lib/db/schema"; export async function GET( _request: NextRequest, - { params }: { params: Promise<{ userId: string }> } + { params }: { params: Promise<{ userId: string }> }, ) { - const { userId: authUserId } = await auth() - const { userId } = await params + const { userId: authUserId } = await auth(); + const { userId } = await params; if (!authUserId || authUserId !== userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } try { @@ -22,24 +22,31 @@ export async function GET( score: submissions.score, }) .from(submissions) - .where(eq(submissions.userId, userId)) + .where(eq(submissions.userId, userId)); - const exerciseScores = new Map() + const exerciseScores = new Map(); userSubmissions.forEach((sub) => { - const current = exerciseScores.get(sub.exerciseId) || 0 - exerciseScores.set(sub.exerciseId, Math.max(current, sub.score)) - }) + const current = exerciseScores.get(sub.exerciseId) || 0; + exerciseScores.set(sub.exerciseId, Math.max(current, sub.score)); + }); const totalScore = Array.from(exerciseScores.values()).reduce( (sum, score) => sum + score, - 0 - ) + 0, + ); - console.log('Score calculated:', { userId, exerciseCount: exerciseScores.size, totalScore }) + console.log("Score calculated:", { + userId, + exerciseCount: exerciseScores.size, + totalScore, + }); - return NextResponse.json({ score: totalScore }) + return NextResponse.json({ score: totalScore }); } catch (error) { - console.error('Error fetching score:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + console.error("Error fetching score:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } diff --git a/src/app/api/users/[userId]/solutions/[exerciseId]/route.ts b/src/app/api/users/[userId]/solutions/[exerciseId]/route.ts index 5c03668..e925539 100644 --- a/src/app/api/users/[userId]/solutions/[exerciseId]/route.ts +++ b/src/app/api/users/[userId]/solutions/[exerciseId]/route.ts @@ -1,18 +1,18 @@ -import { NextRequest, NextResponse } from 'next/server' -import { auth } from '@clerk/nextjs/server' -import { db } from '@/lib/db' -import { submissions } from '@/lib/db/schema' -import { eq, and, desc } from 'drizzle-orm' +import { auth } from "@clerk/nextjs/server"; +import { and, desc, eq } from "drizzle-orm"; +import { type NextRequest, NextResponse } from "next/server"; +import { db } from "@/lib/db"; +import { submissions } from "@/lib/db/schema"; export async function GET( _request: NextRequest, - { params }: { params: Promise<{ userId: string; exerciseId: string }> } + { params }: { params: Promise<{ userId: string; exerciseId: string }> }, ) { - const { userId: authUserId } = await auth() - const { userId, exerciseId } = await params + const { userId: authUserId } = await auth(); + const { userId, exerciseId } = await params; if (!authUserId || authUserId !== userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } try { @@ -22,16 +22,18 @@ export async function GET( .where( and( eq(submissions.userId, userId), - eq(submissions.exerciseId, exerciseId) - ) + eq(submissions.exerciseId, exerciseId), + ), ) .orderBy(desc(submissions.createdAt)) - .limit(1) + .limit(1); - return NextResponse.json({ solution: submission?.solution || null }) + return NextResponse.json({ solution: submission?.solution || null }); } catch (error) { - console.error('Error fetching solution:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + console.error("Error fetching solution:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } - diff --git a/src/app/api/users/[userId]/solved-exercises/route.ts b/src/app/api/users/[userId]/solved-exercises/route.ts index a85906e..3a77ba5 100644 --- a/src/app/api/users/[userId]/solved-exercises/route.ts +++ b/src/app/api/users/[userId]/solved-exercises/route.ts @@ -1,33 +1,42 @@ -import { NextRequest, NextResponse } from 'next/server' -import { auth } from '@clerk/nextjs/server' -import { db } from '@/lib/db' -import { submissions } from '@/lib/db/schema' -import { eq } from 'drizzle-orm' +import { auth } from "@clerk/nextjs/server"; +import { eq } from "drizzle-orm"; +import { type NextRequest, NextResponse } from "next/server"; +import { db } from "@/lib/db"; +import { submissions } from "@/lib/db/schema"; export async function GET( _request: NextRequest, - { params }: { params: Promise<{ userId: string }> } + { params }: { params: Promise<{ userId: string }> }, ) { - const { userId: authUserId } = await auth() - const { userId } = await params + const { userId: authUserId } = await auth(); + const { userId } = await params; if (!authUserId || authUserId !== userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } try { const userSubmissions = await db .select({ exerciseId: submissions.exerciseId }) .from(submissions) - .where(eq(submissions.userId, userId)) + .where(eq(submissions.userId, userId)); - const exerciseIds = [...new Set(userSubmissions.map((sub) => sub.exerciseId))] + const exerciseIds = [ + ...new Set(userSubmissions.map((sub) => sub.exerciseId)), + ]; - console.log('Fetched solved exercises:', { userId, count: exerciseIds.length, exerciseIds }) + console.log("Fetched solved exercises:", { + userId, + count: exerciseIds.length, + exerciseIds, + }); - return NextResponse.json({ exerciseIds }) + return NextResponse.json({ exerciseIds }); } catch (error) { - console.error('Error fetching solved exercises:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + console.error("Error fetching solved exercises:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } diff --git a/src/app/api/users/[userId]/streak/route.ts b/src/app/api/users/[userId]/streak/route.ts index 4990c56..b2c7da8 100644 --- a/src/app/api/users/[userId]/streak/route.ts +++ b/src/app/api/users/[userId]/streak/route.ts @@ -1,18 +1,18 @@ -import { NextRequest, NextResponse } from 'next/server' -import { auth } from '@clerk/nextjs/server' -import { db } from '@/lib/db' -import { submissions } from '@/lib/db/schema' -import { eq, desc } from 'drizzle-orm' +import { auth } from "@clerk/nextjs/server"; +import { desc, eq } from "drizzle-orm"; +import { type NextRequest, NextResponse } from "next/server"; +import { db } from "@/lib/db"; +import { submissions } from "@/lib/db/schema"; export async function GET( _request: NextRequest, - { params }: { params: Promise<{ userId: string }> } + { params }: { params: Promise<{ userId: string }> }, ) { - const { userId: authUserId } = await auth() - const { userId } = await params + const { userId: authUserId } = await auth(); + const { userId } = await params; if (!authUserId || authUserId !== userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } try { @@ -20,53 +20,60 @@ export async function GET( .select({ createdAt: submissions.createdAt }) .from(submissions) .where(eq(submissions.userId, userId)) - .orderBy(desc(submissions.createdAt)) + .orderBy(desc(submissions.createdAt)); if (userSubmissions.length === 0) { - return NextResponse.json({ streak: 0 }) + return NextResponse.json({ streak: 0 }); } - const today = new Date() - today.setHours(0, 0, 0, 0) + const today = new Date(); + today.setHours(0, 0, 0, 0); const submissionDates = userSubmissions.map((sub) => { - const date = new Date(sub.createdAt) - date.setHours(0, 0, 0, 0) - return date - }) + const date = new Date(sub.createdAt); + date.setHours(0, 0, 0, 0); + return date; + }); const hasSubmittedToday = submissionDates.some( - (date) => date.getTime() === today.getTime() - ) + (date) => date.getTime() === today.getTime(), + ); if (!hasSubmittedToday) { - const yesterday = new Date(today) - yesterday.setDate(yesterday.getDate() - 1) + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); const hasSubmittedYesterday = submissionDates.some( - (date) => date.getTime() === yesterday.getTime() - ) + (date) => date.getTime() === yesterday.getTime(), + ); if (!hasSubmittedYesterday) { - return NextResponse.json({ streak: 0 }) + return NextResponse.json({ streak: 0 }); } } - let streak = 0 - const startDate = hasSubmittedToday ? today : new Date(submissionDates[0]) - const checkDate = new Date(startDate) + let streak = 0; + const startDate = hasSubmittedToday ? today : new Date(submissionDates[0]); + const checkDate = new Date(startDate); while (true) { const exists = submissionDates.some( - (date) => date.getTime() === checkDate.getTime() - ) - if (!exists) break - streak++ - checkDate.setDate(checkDate.getDate() - 1) + (date) => date.getTime() === checkDate.getTime(), + ); + if (!exists) break; + streak++; + checkDate.setDate(checkDate.getDate() - 1); } - console.log('Streak calculated:', { userId, streak, submissionCount: userSubmissions.length }) - return NextResponse.json({ streak }) + console.log("Streak calculated:", { + userId, + streak, + submissionCount: userSubmissions.length, + }); + return NextResponse.json({ streak }); } catch (error) { - console.error('Error calculating streak:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + console.error("Error calculating streak:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } diff --git a/src/app/api/users/[userId]/week-progress/route.ts b/src/app/api/users/[userId]/week-progress/route.ts index ac8a994..1ecfd59 100644 --- a/src/app/api/users/[userId]/week-progress/route.ts +++ b/src/app/api/users/[userId]/week-progress/route.ts @@ -1,25 +1,25 @@ -import { NextRequest, NextResponse } from 'next/server' -import { auth } from '@clerk/nextjs/server' -import { db } from '@/lib/db' -import { submissions } from '@/lib/db/schema' -import { eq, gte, and } from 'drizzle-orm' +import { auth } from "@clerk/nextjs/server"; +import { and, eq, gte } from "drizzle-orm"; +import { type NextRequest, NextResponse } from "next/server"; +import { db } from "@/lib/db"; +import { submissions } from "@/lib/db/schema"; export async function GET( _request: NextRequest, - { params }: { params: Promise<{ userId: string }> } + { params }: { params: Promise<{ userId: string }> }, ) { - const { userId: authUserId } = await auth() - const { userId } = await params + const { userId: authUserId } = await auth(); + const { userId } = await params; if (!authUserId || authUserId !== userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } try { - const today = new Date() - const startOfWeek = new Date(today) - startOfWeek.setDate(today.getDate() - today.getDay()) - startOfWeek.setHours(0, 0, 0, 0) + const today = new Date(); + const startOfWeek = new Date(today); + startOfWeek.setDate(today.getDate() - today.getDay()); + startOfWeek.setHours(0, 0, 0, 0); const userSubmissions = await db .select({ createdAt: submissions.createdAt }) @@ -27,31 +27,34 @@ export async function GET( .where( and( eq(submissions.userId, userId), - gte(submissions.createdAt, startOfWeek) - ) - ) + gte(submissions.createdAt, startOfWeek), + ), + ); const daysWithSubmissions = new Set( userSubmissions.map((sub) => { - const date = new Date(sub.createdAt) - date.setHours(0, 0, 0, 0) - return date.getTime() - }) - ) + const date = new Date(sub.createdAt); + date.setHours(0, 0, 0, 0); + return date.getTime(); + }), + ); - const weekProgress = [] + const weekProgress = []; for (let i = 0; i < 7; i++) { - const date = new Date(startOfWeek) - date.setDate(startOfWeek.getDate() + i) + const date = new Date(startOfWeek); + date.setDate(startOfWeek.getDate() + i); weekProgress.push({ - day: date.toLocaleDateString('en-US', { weekday: 'short' }), + day: date.toLocaleDateString("en-US", { weekday: "short" }), completed: daysWithSubmissions.has(date.getTime()), - }) + }); } - return NextResponse.json({ progress: weekProgress }) + return NextResponse.json({ progress: weekProgress }); } catch (error) { - console.error('Error fetching week progress:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + console.error("Error fetching week progress:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } diff --git a/src/app/docs/layout.tsx b/src/app/docs/layout.tsx index 8ba584e..a865da6 100644 --- a/src/app/docs/layout.tsx +++ b/src/app/docs/layout.tsx @@ -1,19 +1,16 @@ -import type { Metadata } from 'next' +import type { Metadata } from "next"; export const metadata: Metadata = { - title: 'Documentación - SQL4All', - description: 'Guía completa de SQL y uso de la plataforma SQL4All', -} + title: "Documentación - SQL4All", + description: "Guía completa de SQL y uso de la plataforma SQL4All", +}; export default function DocsLayout({ children, }: { - children: React.ReactNode + children: React.ReactNode; }) { return ( -
- {children} -
- ) +
{children}
+ ); } - diff --git a/src/app/docs/page.tsx b/src/app/docs/page.tsx index 01f8a46..621c39f 100644 --- a/src/app/docs/page.tsx +++ b/src/app/docs/page.tsx @@ -1,54 +1,54 @@ -'use client' +"use client"; -import { useState } from 'react' -import { motion } from 'framer-motion' -import { - BookOpen, - Database, - Code2, - Search, +import { motion } from "framer-motion"; +import { + ArrowUpDown, + BookOpen, + Calculator, ChevronRight, - Table2, + Code2, + Database, Filter, - ArrowUpDown, + Layers, Link2, - Calculator, - Layers -} from 'lucide-react' + Search, + Table2, +} from "lucide-react"; +import { useState } from "react"; +import { SchemaViewer } from "@/components/docs/schema-viewer"; +import { SqlReference } from "@/components/docs/sql-reference"; import { + Badge, Card, CardContent, CardDescription, CardHeader, CardTitle, Input, - Badge, Tabs, TabsContent, TabsList, TabsTrigger, -} from '@/components/ui' -import { SchemaViewer } from '@/components/docs/schema-viewer' -import { SqlReference } from '@/components/docs/sql-reference' +} from "@/components/ui"; const sections = [ - { id: 'intro', label: 'Introducción', icon: BookOpen }, - { id: 'schema', label: 'Esquema de Datos', icon: Database }, - { id: 'commands', label: 'Comandos SQL', icon: Code2 }, -] + { id: "intro", label: "Introducción", icon: BookOpen }, + { id: "schema", label: "Esquema de Datos", icon: Database }, + { id: "commands", label: "Comandos SQL", icon: Code2 }, +]; const quickLinks = [ - { label: 'SELECT', href: '#select', icon: Table2 }, - { label: 'WHERE', href: '#where', icon: Filter }, - { label: 'ORDER BY', href: '#orderby', icon: ArrowUpDown }, - { label: 'JOIN', href: '#join', icon: Link2 }, - { label: 'Agregaciones', href: '#aggregations', icon: Calculator }, - { label: 'GROUP BY', href: '#groupby', icon: Layers }, -] + { label: "SELECT", href: "#select", icon: Table2 }, + { label: "WHERE", href: "#where", icon: Filter }, + { label: "ORDER BY", href: "#orderby", icon: ArrowUpDown }, + { label: "JOIN", href: "#join", icon: Link2 }, + { label: "Agregaciones", href: "#aggregations", icon: Calculator }, + { label: "GROUP BY", href: "#groupby", icon: Layers }, +]; export default function DocsPage() { - const [searchQuery, setSearchQuery] = useState('') - const [activeSection, setActiveSection] = useState('intro') + const [searchQuery, setSearchQuery] = useState(""); + const [activeSection, setActiveSection] = useState("intro"); return (
@@ -67,15 +67,15 @@ export default function DocsPage() { @@ -99,9 +99,9 @@ export default function DocsPage() { variant="secondary" className="cursor-pointer hover:bg-primary/10 transition-colors" onClick={() => { - setActiveSection('commands') - const element = document.querySelector(link.href) - element?.scrollIntoView({ behavior: 'smooth' }) + setActiveSection("commands"); + const element = document.querySelector(link.href); + element?.scrollIntoView({ behavior: "smooth" }); }} > {link.label} @@ -119,13 +119,15 @@ export default function DocsPage() { animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }} > - {activeSection === 'intro' && } - {activeSection === 'schema' && } - {activeSection === 'commands' && } + {activeSection === "intro" && } + {activeSection === "schema" && } + {activeSection === "commands" && ( + + )}
- ) + ); } function IntroSection() { @@ -141,7 +143,10 @@ function IntroSection() {

- Basado en PostgreSQL - Aprende SQL con la sintaxis y características de PostgreSQL + + Basado en PostgreSQL - + Aprende SQL con la sintaxis y características de PostgreSQL +
@@ -155,7 +160,10 @@ function IntroSection() {

SQL (Structured Query Language) es el lenguaje estándar para - manipular y consultar bases de datos relacionales. En SQL4All aprenderás SQL usando PostgreSQL, uno de los sistemas de bases de datos más populares y potentes. Te permite: + manipular y consultar bases de datos relacionales. En SQL4All + aprenderás SQL usando{" "} + PostgreSQL, uno de los + sistemas de bases de datos más populares y potentes. Te permite: