diff --git a/Dockerfile b/Dockerfile index 5193ff8..99a56ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,9 +6,10 @@ RUN apk add --no-cache openssl WORKDIR /app -# Copy package files +# Copy package files and scripts (needed for postinstall) COPY package*.json ./ COPY prisma ./prisma/ +COPY scripts ./scripts/ # Install dependencies RUN npm ci @@ -42,11 +43,11 @@ COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/.next ./.next COPY --from=builder /app/public ./public COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/prisma.config.ts ./ COPY --from=builder /app/server.ts ./ COPY --from=builder /app/src ./src COPY --from=builder /app/tsconfig.json ./ COPY --from=builder /app/next.config.mjs ./ -COPY --from=builder /app/tailwind.config.ts ./ COPY --from=builder /app/postcss.config.mjs ./ # Create data directory for SQLite @@ -58,7 +59,7 @@ USER nextjs EXPOSE 3000 ENV PORT=3000 -ENV DATABASE_URL="file:./data/quiz.db" +ENV DATABASE_URL="file:./prisma/data/quiz.db" # Start the server CMD ["sh", "-c", "npx prisma migrate deploy && npm run start"] diff --git a/components.json b/components.json index dba50c6..fc0433c 100644 --- a/components.json +++ b/components.json @@ -4,7 +4,6 @@ "rsc": true, "tsx": true, "tailwind": { - "config": "tailwind.config.ts", "css": "src/app/globals.css", "baseColor": "slate", "cssVariables": true, diff --git a/docker-compose.yml b/docker-compose.yml index 7a3ff6e..9928595 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,26 @@ services: quiz0r: - image: quiz0r:latest container_name: quiz0r + build: context: . target: runner + + image: quiz0r:latest + + restart: unless-stopped + ports: - - "3000:3000" + - "18081:3000" + + environment: + NODE_ENV: production + DATABASE_URL: file:./prisma/data/quiz.db + TZ: Europe/Berlin + volumes: - # Persist SQLite database - quiz-data:/app/prisma/data - # Persist uploaded images (if used) - quiz-uploads:/app/public/uploads - environment: - - NODE_ENV=production - - DATABASE_URL=file:./data/quiz.db - restart: unless-stopped volumes: quiz-data: diff --git a/next.config.mjs b/next.config.mjs index d8fd9e3..e94439a 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,10 +1,7 @@ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, - experimental: { - // Externalize @ngrok/ngrok to avoid webpack bundling native modules - serverComponentsExternalPackages: ["@ngrok/ngrok"], - }, + serverExternalPackages: ["@ngrok/ngrok"], images: { remotePatterns: [ { diff --git a/package-lock.json b/package-lock.json index b3b094c..5cc92b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,9 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^5.4.0", + "@libsql/client": "^0.17.4", "@ngrok/ngrok": "^1.7.0", + "@prisma/adapter-libsql": "^7.9.1", "@prisma/client": "^7.8.0", "@radix-ui/react-alert-dialog": "^1.1.17", "@radix-ui/react-avatar": "^1.2.0", @@ -28,6 +30,7 @@ "@radix-ui/react-switch": "^1.3.1", "@radix-ui/react-tabs": "^1.1.15", "@radix-ui/react-tooltip": "^1.2.10", + "@tailwindcss/postcss": "^4.3.3", "@types/node": "^26.0.0", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", @@ -35,6 +38,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.4.0", + "dotenv": "^17.4.2", "file-saver": "^2.0.5", "html2canvas": "^1.4.1", "jszip": "^3.10.1", @@ -51,7 +55,6 @@ "socket.io-client": "^4.8.3", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", - "tailwindcss-animate": "^1.0.7", "typescript": "^6.0.3", "zod": "^4.4.3" }, @@ -66,6 +69,7 @@ "eslint": "^10.5.0", "eslint-config-next": "^16.2.9", "postcss": "^8.5.15", + "tailwind-animate": "^0.2.10", "tailwindcss": "^4.3.1", "tsx": "^4.22.4", "vitest": "^4.1.9" @@ -78,6 +82,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -418,7 +434,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -440,7 +455,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1203,9 +1217,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1222,9 +1233,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1241,9 +1249,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1260,9 +1265,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1279,9 +1281,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1298,9 +1297,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1317,9 +1313,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1336,9 +1329,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1355,9 +1345,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1380,9 +1367,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1405,9 +1389,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1430,9 +1411,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1455,9 +1433,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1480,9 +1455,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1505,9 +1477,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1530,9 +1499,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1628,7 +1594,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1639,7 +1604,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1650,7 +1614,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1660,14 +1623,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1680,11 +1641,169 @@ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, + "node_modules/@libsql/client": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.17.4.tgz", + "integrity": "sha512-lYayFWasDV78A+TjlEhr6ubb3odBV6OHjb+wdp8VQcyWWAEIjuwbCHaraEUS4m4yWoo0BvZo96It4VdzZRmRWw==", + "license": "MIT", + "dependencies": { + "@libsql/core": "^0.17.4", + "@libsql/hrana-client": "^0.10.0", + "js-base64": "^3.7.5", + "libsql": "^0.5.28", + "promise-limit": "^2.7.0" + } + }, + "node_modules/@libsql/core": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@libsql/core/-/core-0.17.4.tgz", + "integrity": "sha512-LqF9gIvnJ38nmAH1y/ChizHqDO/MO1wLgA96XrraulEEbqXxLjleSH92YWTolbuJKgPUmGu4aJk9W3UnAcxLOQ==", + "license": "MIT", + "dependencies": { + "js-base64": "^3.7.5" + } + }, + "node_modules/@libsql/darwin-arm64": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.5.29.tgz", + "integrity": "sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@libsql/darwin-x64": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.5.29.tgz", + "integrity": "sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@libsql/hrana-client": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@libsql/hrana-client/-/hrana-client-0.10.0.tgz", + "integrity": "sha512-OoA4EMqRAC7kn7V2P6EQqRcpZf2W+AjsNIyCizBg339Tq/aMC7sRnzs3SklderhmQWAqEzvv8A2vhxVmWpkVvw==", + "license": "MIT", + "dependencies": { + "@libsql/isomorphic-ws": "^0.1.5", + "js-base64": "^3.7.5" + } + }, + "node_modules/@libsql/isomorphic-ws": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@libsql/isomorphic-ws/-/isomorphic-ws-0.1.5.tgz", + "integrity": "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==", + "license": "MIT", + "dependencies": { + "@types/ws": "^8.5.4", + "ws": "^8.13.0" + } + }, + "node_modules/@libsql/linux-arm-gnueabihf": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-arm-gnueabihf/-/linux-arm-gnueabihf-0.5.29.tgz", + "integrity": "sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-arm-musleabihf": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-arm-musleabihf/-/linux-arm-musleabihf-0.5.29.tgz", + "integrity": "sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-arm64-gnu": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.5.29.tgz", + "integrity": "sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-arm64-musl": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.5.29.tgz", + "integrity": "sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-x64-gnu": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.5.29.tgz", + "integrity": "sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-x64-musl": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.5.29.tgz", + "integrity": "sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/win32-x64-msvc": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.5.29.tgz", + "integrity": "sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1699,6 +1818,12 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@neon-rs/load": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.0.4.tgz", + "integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==", + "license": "MIT" + }, "node_modules/@next/env": { "version": "16.2.9", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.9.tgz", @@ -1754,9 +1879,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1773,9 +1895,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1792,9 +1911,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1811,9 +1927,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1979,9 +2092,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1998,9 +2108,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2017,9 +2124,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2036,9 +2140,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2170,6 +2271,17 @@ "node": ">=18" } }, + "node_modules/@prisma/adapter-libsql": { + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/@prisma/adapter-libsql/-/adapter-libsql-7.9.1.tgz", + "integrity": "sha512-bHfu0fnxF5ALP/AZLM0TyfhFjv9H/g0lRCt3j2aVuLMSQy6zgy3OAeWkp+UoLx+dP9Yuz+EsNXos6IN2e2eyvQ==", + "license": "Apache-2.0", + "dependencies": { + "@libsql/client": "^0.17.0", + "@prisma/driver-adapter-utils": "7.9.1", + "async-mutex": "0.5.0" + } + }, "node_modules/@prisma/client": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.8.0.tgz", @@ -2243,6 +2355,21 @@ "zeptomatch": "2.1.0" } }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.9.1.tgz", + "integrity": "sha512-vmHehG7nn/heW32DXXpp13DxxAxVVe6n250oEt3dOL2E/4bt3olktKZN0mzSuxMMronyMSkbeW2uCOn3F4g8RQ==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.9.1" + } + }, + "node_modules/@prisma/driver-adapter-utils/node_modules/@prisma/debug": { + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.9.1.tgz", + "integrity": "sha512-/cpVZ4itxtcgB8GHBvZtcmuEjq+lWsLrRJxFMbwZrT1RIdtuKmUm7PPGo/wzfbYpBrk+9WmmBE8CHJw2rybKDQ==", + "license": "Apache-2.0" + }, "node_modules/@prisma/engines": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.8.0.tgz", @@ -3557,9 +3684,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3577,9 +3701,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3597,9 +3718,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3617,9 +3735,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3637,9 +3752,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3657,9 +3769,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3830,6 +3939,262 @@ "tslib": "^2.8.0" } }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", @@ -3935,7 +4300,6 @@ "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4038,6 +4402,15 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.0.tgz", @@ -4387,9 +4760,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4404,9 +4774,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4421,9 +4788,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4438,9 +4802,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4455,9 +4816,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4472,9 +4830,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4489,9 +4844,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4506,9 +4858,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4523,9 +4872,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4540,9 +4886,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5083,6 +5426,15 @@ "node": ">= 0.4" } }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/autoprefixer": { "version": "10.5.1", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.1.tgz", @@ -5329,15 +5681,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/c12/node_modules/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/c12/node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", @@ -5784,7 +6127,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -6002,6 +6344,19 @@ } } }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/env-paths": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", @@ -7928,6 +8283,21 @@ "node": ">= 0.4" } }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-base64": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.9.2.tgz", + "integrity": "sha512-6zayE8QlUdiweYI6cETD/XBSqFcoCUlufn/29PJR99r82x1yDnIprRca0YvAYpAW+ez0GuQkVBC6xG5QkD7OjA==", + "license": "BSD-3-Clause" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8053,6 +8423,47 @@ "node": ">= 0.8.0" } }, + "node_modules/libsql": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/libsql/-/libsql-0.5.29.tgz", + "integrity": "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==", + "cpu": [ + "x64", + "arm64", + "wasm32", + "arm" + ], + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "@neon-rs/load": "^0.0.4", + "detect-libc": "2.0.2" + }, + "optionalDependencies": { + "@libsql/darwin-arm64": "0.5.29", + "@libsql/darwin-x64": "0.5.29", + "@libsql/linux-arm-gnueabihf": "0.5.29", + "@libsql/linux-arm-musleabihf": "0.5.29", + "@libsql/linux-arm64-gnu": "0.5.29", + "@libsql/linux-arm64-musl": "0.5.29", + "@libsql/linux-x64-gnu": "0.5.29", + "@libsql/linux-x64-musl": "0.5.29", + "@libsql/win32-x64-msvc": "0.5.29" + } + }, + "node_modules/libsql/node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -8065,7 +8476,6 @@ "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -8098,7 +8508,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8119,7 +8528,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8140,7 +8548,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8161,7 +8568,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8182,7 +8588,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8203,10 +8608,6 @@ "cpu": [ "arm64" ], - "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8227,10 +8628,6 @@ "cpu": [ "arm64" ], - "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8251,10 +8648,6 @@ "cpu": [ "x64" ], - "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8275,10 +8668,6 @@ "cpu": [ "x64" ], - "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8299,7 +8688,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8320,7 +8708,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8428,7 +8815,6 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -9129,6 +9515,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -9150,10 +9537,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "dev": true, + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -9170,7 +9556,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -9185,10 +9571,9 @@ "license": "MIT" }, "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", - "dev": true, + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", "funding": [ { "type": "github", @@ -9302,6 +9687,12 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/promise-limit": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz", + "integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==", + "license": "ISC" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -10421,6 +10812,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-animate": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/tailwind-animate/-/tailwind-animate-0.2.10.tgz", + "integrity": "sha512-9vczT78R9O7HIrydvGJNz4/19ogjSQSFlYpwleAxcxIT1m9pvAIGQr/+f6qG9sB5gq3JLB6IRKmw9RGJUrLeXQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/nrjdalal" + } + }, "node_modules/tailwind-merge": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", @@ -10432,18 +10833,22 @@ } }, "node_modules/tailwindcss": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", - "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", "license": "MIT" }, - "node_modules/tailwindcss-animate": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", - "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "license": "MIT", - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders" + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/text-segmentation": { @@ -11272,6 +11677,27 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.21.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.2.tgz", + "integrity": "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xmlhttprequest-ssl": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", diff --git a/package.json b/package.json index 44fae2f..c5efded 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "test:watch": "vitest watch", "test:e2e": "playwright test -c testing/playwright.config.ts", "db:push": "prisma db push", - "db:studio": "prisma studio" + "db:studio": "prisma studio", + "postinstall": "node scripts/patch-html2canvas.js" }, "description": "Real-time multiplayer quiz application", "dependencies": { @@ -20,7 +21,9 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^5.4.0", + "@libsql/client": "^0.17.4", "@ngrok/ngrok": "^1.7.0", + "@prisma/adapter-libsql": "^7.9.1", "@prisma/client": "^7.8.0", "@radix-ui/react-alert-dialog": "^1.1.17", "@radix-ui/react-avatar": "^1.2.0", @@ -36,6 +39,7 @@ "@radix-ui/react-switch": "^1.3.1", "@radix-ui/react-tabs": "^1.1.15", "@radix-ui/react-tooltip": "^1.2.10", + "@tailwindcss/postcss": "^4.3.3", "@types/node": "^26.0.0", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", @@ -43,6 +47,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.4.0", + "dotenv": "^17.4.2", "file-saver": "^2.0.5", "html2canvas": "^1.4.1", "jszip": "^3.10.1", @@ -59,23 +64,23 @@ "socket.io-client": "^4.8.3", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", - "tailwindcss-animate": "^1.0.7", "typescript": "^6.0.3", "zod": "^4.4.3" }, "devDependencies": { + "@playwright/test": "^1.61.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.5.2", "@types/file-saver": "^2.0.7", + "@vitest/coverage-v8": "^4.1.9", "autoprefixer": "^10.5.1", "eslint": "^10.5.0", "eslint-config-next": "^16.2.9", "postcss": "^8.5.15", + "tailwind-animate": "^0.2.10", "tailwindcss": "^4.3.1", "tsx": "^4.22.4", - "@vitest/coverage-v8": "^4.1.9", - "vitest": "^4.1.9", - "@testing-library/react": "^16.3.2", - "@testing-library/jest-dom": "^6.6.3", - "@testing-library/user-event": "^14.5.2", - "@playwright/test": "^1.61.1" + "vitest": "^4.1.9" } -} \ No newline at end of file +} diff --git a/postcss.config.mjs b/postcss.config.mjs index 2ef30fc..79bcf13 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,8 +1,7 @@ /** @type {import('postcss-load-config').Config} */ const config = { plugins: { - tailwindcss: {}, - autoprefixer: {}, + "@tailwindcss/postcss": {}, }, }; diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..9e8995b --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,12 @@ +import "dotenv/config"; +import { defineConfig } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: process.env["DATABASE_URL"] ?? "file:./prisma/data/quiz.db", + }, +}); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 07b481c..7b4d4c6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -4,7 +4,6 @@ generator client { datasource db { provider = "sqlite" - url = env("DATABASE_URL") } // Saved themes (user-created) diff --git a/scripts/patch-html2canvas.js b/scripts/patch-html2canvas.js new file mode 100644 index 0000000..64aa721 --- /dev/null +++ b/scripts/patch-html2canvas.js @@ -0,0 +1,91 @@ +/** + * Postinstall script to patch html2canvas for Tailwind CSS 4 compatibility. + * + * Tailwind CSS 4 generates CSS with lab() and oklab() color functions, + * which html2canvas 1.4.1 doesn't support by default. + * + * This script patches the html2canvas source to add lab/oklab support. + */ + +const fs = require('fs'); +const path = require('path'); + +const filePath = path.join(__dirname, '..', 'node_modules', 'html2canvas', 'dist', 'html2canvas.js'); + +try { + let content = fs.readFileSync(filePath, 'utf8'); + + // Check if already patched + if (content.includes('labRgbFunc')) { + console.log('[patch-html2canvas] Already patched, skipping'); + process.exit(0); + } + + // Find the SUPPORTED_COLOR_FUNCTIONS definition + const regex = /var SUPPORTED_COLOR_FUNCTIONS = \{[^}]+\}/; + const match = content.match(regex); + + if (!match) { + console.error('[patch-html2canvas] ERROR: Could not find SUPPORTED_COLOR_FUNCTIONS'); + process.exit(1); + } + + // LAB to RGB conversion function + const labFunc = `var labRgbFunc = function(context, args) { + var tokens = args.filter(nonFunctionArgSeparator); + var L = tokens[0], a = tokens[1], b = tokens[2], alpha = tokens[3]; + var l = isLengthPercentage(L) ? L.number / 100 * 100 : 0; + var aa = isLengthPercentage(a) ? a.number : (a && a.number ? a.number : 0); + var bb = isLengthPercentage(b) ? b.number : (b && b.number ? b.number : 0); + var al = typeof alpha !== 'undefined' && isLengthPercentage(alpha) ? alpha.number : 1; + var y = (l + 16) / 116; + var x = aa / 500 + y; + var z = y - bb / 200; + var f = function(t) { return t > 0.206893 ? t * t * t : (t - 16 / 116) / 7.787; }; + x = f(x) * 0.95047; y = f(y) * 1.0; z = f(z) * 1.08883; + var r = x * 3.2406 + y * -1.5372 + z * -0.4986; + var g = x * -0.9689 + y * 1.8758 + z * 0.0415; + var bv = x * 0.0557 + y * -0.204 + z * 1.057; + var gc = function(v) { return v > 0.0031308 ? 1.055 * Math.pow(v, 1/2.4) - 0.055 : 12.92 * v; }; + return pack(Math.round(Math.max(0, Math.min(1, gc(r))) * 255), Math.round(Math.max(0, Math.min(1, gc(g))) * 255), Math.round(Math.max(0, Math.min(1, gc(bv))) * 255), al); + };`; + + // OKLAB to RGB conversion function + const oklabFunc = `var oklabRgbFunc = function(context, args) { + var tokens = args.filter(nonFunctionArgSeparator); + var L = tokens[0], a = tokens[1], b = tokens[2], alpha = tokens[3]; + var l = isLengthPercentage(L) ? L.number / 100 * 100 : 0; + var aa = isLengthPercentage(a) ? a.number : (a && a.number ? a.number : 0); + var bb = isLengthPercentage(b) ? b.number : (b && b.number ? b.number : 0); + var al = typeof alpha !== 'undefined' && isLengthPercentage(alpha) ? alpha.number : 1; + var l_ = l + 0.3963377774 * aa + 0.2158037573 * bb; + var m_ = l - 0.1055613458 * aa - 0.0638541728 * bb; + var s_ = l - 0.0894841775 * aa - 1.291485548 * bb; + var lD = l_ * l_ * l_, mD = m_ * m_ * m_, sD = s_ * s_ * s_; + var r = 4.0767416621 * lD - 3.3077115913 * mD + 0.2309699292 * sD; + var g = -1.2684380046 * lD + 2.6097574011 * mD - 0.3413193965 * sD; + var bv = -0.0041960863 * lD - 0.7034186147 * mD + 1.707614701 * sD; + var gc = function(v) { return v > 0.0031308 ? 1.055 * Math.pow(v, 1/2.4) - 0.055 : 12.92 * v; }; + return pack(Math.round(Math.max(0, Math.min(1, gc(r))) * 255), Math.round(Math.max(0, Math.min(1, gc(g))) * 255), Math.round(Math.max(0, Math.min(1, gc(bv))) * 255), al); + };`; + + // Updated SUPPORTED_COLOR_FUNCTIONS + const newSupported = `var SUPPORTED_COLOR_FUNCTIONS = { + hsl: hsl, + hsla: hsl, + rgb: rgb, + rgba: rgb, + lab: labRgbFunc, + oklab: oklabRgbFunc + };`; + + const patch = labFunc + oklabFunc + newSupported; + content = content.replace(match[0], patch); + fs.writeFileSync(filePath, content); + console.log('[patch-html2canvas] Successfully patched html2canvas for lab/oklab support'); +} catch (err) { + console.error('[patch-html2canvas] Error:', err.message); + // Don't fail the install, just warn + console.warn('[patch-html2canvas] Warning: Could not patch html2canvas. html2canvas features may not work with Tailwind CSS 4.'); + process.exit(0); +} diff --git a/server.ts b/server.ts index 671decf..dd3d419 100644 --- a/server.ts +++ b/server.ts @@ -6,7 +6,7 @@ import { GameManager } from "./src/server/game-manager"; import { autoStartTunnel } from "./src/lib/tunnel"; const dev = process.env.NODE_ENV !== "production"; -const hostname = "localhost"; +const hostname = "0.0.0.0"; const port = parseInt(process.env.PORT || "3000", 10); const app = next({ dev, hostname, port }); diff --git a/src/app/admin/quiz/[quizId]/questions/page.tsx b/src/app/admin/quiz/[quizId]/questions/page.tsx index 98f6dea..77b743c 100644 --- a/src/app/admin/quiz/[quizId]/questions/page.tsx +++ b/src/app/admin/quiz/[quizId]/questions/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useRef, useMemo } from "react"; +import { use, useEffect, useState, useRef, useMemo } from "react"; import Link from "next/link"; import { Loader2 } from "lucide-react"; import { DragEndEvent, DragStartEvent } from "@dnd-kit/core"; @@ -120,9 +120,9 @@ function flattenGroups(groups: SectionGroup[]): Question[] { export default function QuestionsPage({ params, }: { - params: { quizId: string }; + params: Promise<{ quizId: string }>; }) { - const { quizId } = params; + const { quizId } = use(params); const [quiz, setQuiz] = useState(null); const [loading, setLoading] = useState(true); const [dialogOpen, setDialogOpen] = useState(false); diff --git a/src/app/admin/quiz/[quizId]/theme/page.tsx b/src/app/admin/quiz/[quizId]/theme/page.tsx index 1a3effc..5670fff 100644 --- a/src/app/admin/quiz/[quizId]/theme/page.tsx +++ b/src/app/admin/quiz/[quizId]/theme/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { use, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; @@ -18,7 +18,7 @@ import { PRESET_LIST, THEME_PRESETS } from "@/lib/theme-presets"; import { ArrowLeft, Check, Loader2, Palette, Plus } from "lucide-react"; interface Props { - params: { quizId: string }; + params: Promise<{ quizId: string }>; } interface ThemeRecord { @@ -38,7 +38,7 @@ type ThemeOption = { }; export default function QuizThemeApplyPage({ params }: Props) { - const { quizId } = params; + const { quizId } = use(params); const router = useRouter(); const [quizTitle, setQuizTitle] = useState(""); diff --git a/src/app/admin/themes/[themeId]/page.tsx b/src/app/admin/themes/[themeId]/page.tsx index 6748fe1..d951687 100644 --- a/src/app/admin/themes/[themeId]/page.tsx +++ b/src/app/admin/themes/[themeId]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { use, useEffect, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; @@ -35,7 +35,7 @@ import { } from "lucide-react"; interface Props { - params: { themeId: string }; + params: Promise<{ themeId: string }>; } type TabType = "presets" | "wizard" | "json"; @@ -65,7 +65,7 @@ function findBuiltinThemeId(themeJson: string | null | undefined): string | null } export default function ThemeBuilderPage({ params }: Props) { - const { themeId } = params; + const { themeId } = use(params); const router = useRouter(); const isNew = themeId === "new"; diff --git a/src/app/api/admin/games/[gameId]/route.ts b/src/app/api/admin/games/[gameId]/route.ts index 5450e30..735e1eb 100644 --- a/src/app/api/admin/games/[gameId]/route.ts +++ b/src/app/api/admin/games/[gameId]/route.ts @@ -3,11 +3,12 @@ import { prisma } from "@/lib/db"; export async function GET( request: NextRequest, - { params }: { params: { gameId: string } } + { params }: { params: Promise<{ gameId: string }> } ) { + const { gameId } = await params; try { const game = await prisma.gameSession.findUnique({ - where: { id: params.gameId }, + where: { id: gameId }, include: { quiz: { select: { @@ -70,11 +71,12 @@ export async function GET( export async function DELETE( _request: NextRequest, - { params }: { params: { gameId: string } } + { params }: { params: Promise<{ gameId: string }> } ) { + const { gameId } = await params; try { const game = await prisma.gameSession.findUnique({ - where: { id: params.gameId }, + where: { id: gameId }, select: { status: true } }); @@ -90,7 +92,7 @@ export async function DELETE( } await prisma.gameSession.delete({ - where: { id: params.gameId } + where: { id: gameId } }); return Response.json({ success: true }); diff --git a/src/app/api/games/[gameCode]/certificates/download/route.ts b/src/app/api/games/[gameCode]/certificates/download/route.ts index 87dbc26..d2f1602 100644 --- a/src/app/api/games/[gameCode]/certificates/download/route.ts +++ b/src/app/api/games/[gameCode]/certificates/download/route.ts @@ -5,8 +5,9 @@ import path from "path"; export async function POST( request: NextRequest, - { params }: { params: { gameCode: string } } + { params }: { params: Promise<{ gameCode: string }> } ) { + const { gameCode } = await params; try { const body = await request.json(); const { type, playerId } = body; @@ -14,7 +15,7 @@ export async function POST( // Find certificate const certificate = await prisma.certificate.findFirst({ where: { - gameSession: { gameCode: params.gameCode.toUpperCase() }, + gameSession: { gameCode: gameCode.toUpperCase() }, type, playerId: type === "player" ? playerId : null, }, diff --git a/src/app/api/games/[gameCode]/certificates/generate/route.ts b/src/app/api/games/[gameCode]/certificates/generate/route.ts index 51e2792..e6acb8b 100644 --- a/src/app/api/games/[gameCode]/certificates/generate/route.ts +++ b/src/app/api/games/[gameCode]/certificates/generate/route.ts @@ -17,13 +17,13 @@ import { QuizTheme, DEFAULT_THEME } from "@/types/theme"; export async function POST( request: NextRequest, - { params }: { params: { gameCode: string } } + { params }: { params: Promise<{ gameCode: string }> } ) { try { // Parse request const body = (await request.json()) as CertificateRequest; const { type, playerId } = body; - const { gameCode } = params; + const { gameCode } = await params; // Fetch game data const gameData = await prisma.gameSession.findUnique({ diff --git a/src/app/api/games/[gameCode]/certificates/regenerate/route.ts b/src/app/api/games/[gameCode]/certificates/regenerate/route.ts index e906a91..e258435 100644 --- a/src/app/api/games/[gameCode]/certificates/regenerate/route.ts +++ b/src/app/api/games/[gameCode]/certificates/regenerate/route.ts @@ -3,8 +3,9 @@ import { CertificateService } from "@/lib/certificate-service"; export async function POST( request: NextRequest, - { params }: { params: { gameCode: string } } + { params }: { params: Promise<{ gameCode: string }> } ) { + const { gameCode } = await params; try { const body = await request.json(); const { certificateIds } = body; diff --git a/src/app/api/games/[gameCode]/certificates/status/route.ts b/src/app/api/games/[gameCode]/certificates/status/route.ts index ef0d5f1..2980c7b 100644 --- a/src/app/api/games/[gameCode]/certificates/status/route.ts +++ b/src/app/api/games/[gameCode]/certificates/status/route.ts @@ -3,12 +3,11 @@ import { CertificateService } from "@/lib/certificate-service"; export async function GET( request: NextRequest, - { params }: { params: { gameCode: string } } + { params }: { params: Promise<{ gameCode: string }> } ) { + const { gameCode } = await params; try { - const status = await CertificateService.getCertificateStatus( - params.gameCode - ); + const status = await CertificateService.getCertificateStatus(gameCode); return NextResponse.json(status); } catch (error) { console.error("Failed to fetch certificate status:", error); diff --git a/src/app/api/quizzes/[quizId]/questions/[questionId]/translate/route.ts b/src/app/api/quizzes/[quizId]/questions/[questionId]/translate/route.ts index f1ca72f..2a1416c 100644 --- a/src/app/api/quizzes/[quizId]/questions/[questionId]/translate/route.ts +++ b/src/app/api/quizzes/[quizId]/questions/[questionId]/translate/route.ts @@ -8,10 +8,10 @@ export const dynamic = "force-dynamic"; // POST /api/quizzes/[quizId]/questions/[questionId]/translate - Translate single question or section export async function POST( request: Request, - { params }: { params: { quizId: string; questionId: string } } + { params }: { params: Promise<{ quizId: string; questionId: string }> } ) { + const { questionId } = await params; try { - const { questionId } = params; const body = await request.json(); const { targetLanguage } = body; diff --git a/src/app/api/quizzes/[quizId]/questions/[questionId]/translations/[language]/route.ts b/src/app/api/quizzes/[quizId]/questions/[questionId]/translations/[language]/route.ts index 399f656..918dd1c 100644 --- a/src/app/api/quizzes/[quizId]/questions/[questionId]/translations/[language]/route.ts +++ b/src/app/api/quizzes/[quizId]/questions/[questionId]/translations/[language]/route.ts @@ -7,10 +7,10 @@ export const dynamic = "force-dynamic"; // PUT /api/quizzes/[quizId]/questions/[questionId]/translations/[language] - Update manual translation export async function PUT( request: Request, - { params }: { params: { quizId: string; questionId: string; language: string } } + { params }: { params: Promise<{ quizId: string; questionId: string; language: string }> } ) { + const { questionId, language } = await params; try { - const { questionId, language } = params; const body = await request.json(); // Validate language @@ -107,10 +107,10 @@ export async function PUT( // DELETE /api/quizzes/[quizId]/questions/[questionId]/translations/[language] - Delete translation export async function DELETE( request: Request, - { params }: { params: { quizId: string; questionId: string; language: string } } + { params }: { params: Promise<{ quizId: string; questionId: string; language: string }> } ) { + const { questionId, language } = await params; try { - const { questionId, language } = params; // Validate language if (!(language in SupportedLanguages)) { diff --git a/src/app/api/quizzes/[quizId]/questions/[questionId]/translations/route.ts b/src/app/api/quizzes/[quizId]/questions/[questionId]/translations/route.ts index 44976e2..ea5fe17 100644 --- a/src/app/api/quizzes/[quizId]/questions/[questionId]/translations/route.ts +++ b/src/app/api/quizzes/[quizId]/questions/[questionId]/translations/route.ts @@ -6,10 +6,10 @@ export const dynamic = "force-dynamic"; // GET /api/quizzes/[quizId]/questions/[questionId]/translations - Get all translations for a question export async function GET( request: Request, - { params }: { params: { quizId: string; questionId: string } } + { params }: { params: Promise<{ quizId: string; questionId: string }> } ) { + const { questionId } = await params; try { - const { questionId } = params; // Fetch question with translations const question = await prisma.question.findUnique({ diff --git a/src/app/api/quizzes/[quizId]/translate/route.ts b/src/app/api/quizzes/[quizId]/translate/route.ts index a737717..f78d231 100644 --- a/src/app/api/quizzes/[quizId]/translate/route.ts +++ b/src/app/api/quizzes/[quizId]/translate/route.ts @@ -7,10 +7,10 @@ export const dynamic = "force-dynamic"; // POST /api/quizzes/[quizId]/translate - Translate entire quiz to target language export async function POST( request: Request, - { params }: { params: { quizId: string } } + { params }: { params: Promise<{ quizId: string }> } ) { + const { quizId } = await params; try { - const { quizId } = params; const body = await request.json(); const { targetLanguage } = body; diff --git a/src/app/api/quizzes/[quizId]/translations/[language]/route.ts b/src/app/api/quizzes/[quizId]/translations/[language]/route.ts index d15e31b..177fc23 100644 --- a/src/app/api/quizzes/[quizId]/translations/[language]/route.ts +++ b/src/app/api/quizzes/[quizId]/translations/[language]/route.ts @@ -7,10 +7,10 @@ export const dynamic = "force-dynamic"; // DELETE /api/quizzes/[quizId]/translations/[language] - Delete all translations for a language export async function DELETE( request: Request, - { params }: { params: { quizId: string; language: string } } + { params }: { params: Promise<{ quizId: string; language: string }> } ) { + const { quizId, language } = await params; try { - const { quizId, language } = params; // Validate language if (!(language in SupportedLanguages)) { diff --git a/src/app/api/quizzes/[quizId]/translations/route.ts b/src/app/api/quizzes/[quizId]/translations/route.ts index 9b3511b..378c1f2 100644 --- a/src/app/api/quizzes/[quizId]/translations/route.ts +++ b/src/app/api/quizzes/[quizId]/translations/route.ts @@ -6,10 +6,10 @@ export const dynamic = "force-dynamic"; // GET /api/quizzes/[quizId]/translations - Get translation status for all languages export async function GET( request: Request, - { params }: { params: { quizId: string } } + { params }: { params: Promise<{ quizId: string }> } ) { + const { quizId } = await params; try { - const { quizId } = params; const statuses = await getQuizTranslationStatus(quizId); diff --git a/src/app/globals.css b/src/app/globals.css index 860e7a9..a24f62f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,6 +1,31 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; +@import "tailwind-animate"; + +@theme { + --color-border: hsl(var(--border)); + --color-input: hsl(var(--input)); + --color-ring: hsl(var(--ring)); + --color-background: hsl(var(--background)); + --color-foreground: hsl(var(--foreground)); + --color-primary: hsl(var(--primary)); + --color-primary-foreground: hsl(var(--primary-foreground)); + --color-secondary: hsl(var(--secondary)); + --color-secondary-foreground: hsl(var(--secondary-foreground)); + --color-destructive: hsl(var(--destructive)); + --color-destructive-foreground: hsl(var(--destructive-foreground)); + --color-muted: hsl(var(--muted)); + --color-muted-foreground: hsl(var(--muted-foreground)); + --color-accent: hsl(var(--accent)); + --color-accent-foreground: hsl(var(--accent-foreground)); + --color-popover: hsl(var(--popover)); + --color-popover-foreground: hsl(var(--popover-foreground)); + --color-card: hsl(var(--card)); + --color-card-foreground: hsl(var(--card-foreground)); + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); + --font-family-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; +} @layer base { :root { diff --git a/src/app/host/[gameCode]/control/page.tsx b/src/app/host/[gameCode]/control/page.tsx index c9fdae6..7a73738 100644 --- a/src/app/host/[gameCode]/control/page.tsx +++ b/src/app/host/[gameCode]/control/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { use } from "react"; import Link from "next/link"; import { useState, useEffect } from "react"; import { useSocket } from "@/hooks/useSocket"; @@ -57,9 +58,9 @@ import { DarkModeToggle } from "@/components/ui/dark-mode-toggle"; export default function HostControlPage({ params, }: { - params: { gameCode: string }; + params: Promise<{ gameCode: string }>; }) { - const { gameCode } = params; + const { gameCode } = use(params); const { connected, gameState, diff --git a/src/app/host/[gameCode]/display/page.tsx b/src/app/host/[gameCode]/display/page.tsx index 161b103..84d1051 100644 --- a/src/app/host/[gameCode]/display/page.tsx +++ b/src/app/host/[gameCode]/display/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { use, useState, useEffect } from "react"; import { QRCodeSVG } from "qrcode.react"; import { useSocket } from "@/hooks/useSocket"; import { Progress } from "@/components/ui/progress"; @@ -18,9 +18,9 @@ import { SupportedLanguages } from "@/types"; export default function HostDisplayPage({ params, }: { - params: { gameCode: string }; + params: Promise<{ gameCode: string }>; }) { - const { gameCode } = params; + const { gameCode } = use(params); const [baseUrl, setBaseUrl] = useState(""); const [shortUrl, setShortUrl] = useState(null); const [urlReady, setUrlReady] = useState(false); diff --git a/src/app/host/[gameCode]/playermonitor/page.tsx b/src/app/host/[gameCode]/playermonitor/page.tsx index d3d5c4f..2e1ecae 100644 --- a/src/app/host/[gameCode]/playermonitor/page.tsx +++ b/src/app/host/[gameCode]/playermonitor/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { use, useEffect, useMemo, useState } from "react"; import { useSocket } from "@/hooks/useSocket"; import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; @@ -25,9 +25,9 @@ const ITEMS_PER_PAGE = 9; export default function PlayerMonitorPage({ params, }: { - params: { gameCode: string }; + params: Promise<{ gameCode: string }>; }) { - const { gameCode } = params; + const { gameCode } = use(params); const { connected, playerViews, requestPlayerViews } = useSocket({ gameCode, role: "host", diff --git a/src/app/play/[gameCode]/page.tsx b/src/app/play/[gameCode]/page.tsx index 34ef09d..6a2035e 100644 --- a/src/app/play/[gameCode]/page.tsx +++ b/src/app/play/[gameCode]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useMemo, useRef } from "react"; +import { use, useState, useEffect, useMemo, useRef } from "react"; import { useRouter } from "next/navigation"; import { useSocket } from "@/hooks/useSocket"; import { useQuizPreloader } from "@/hooks/useQuizPreloader"; @@ -38,9 +38,9 @@ import { getContrastingTextColor } from "@/lib/color-utils"; export default function PlayerGamePage({ params, }: { - params: { gameCode: string }; + params: Promise<{ gameCode: string }>; }) { - const { gameCode } = params; + const { gameCode } = use(params); const router = useRouter(); const [playerName, setPlayerName] = useState(""); const [selectedEmoji, setSelectedEmoji] = useState(null); diff --git a/src/app/uploads/[...path]/route.ts b/src/app/uploads/[...path]/route.ts index 4ae0df9..a9c52f5 100644 --- a/src/app/uploads/[...path]/route.ts +++ b/src/app/uploads/[...path]/route.ts @@ -5,14 +5,15 @@ import path from "path"; // Fallback handler to serve uploaded files even when Next static serving misses. export async function GET( _req: Request, - { params }: { params: { path: string[] } } + { params }: { params: Promise<{ path: string[] }> } ) { + const { path: filePathSegments } = await params; try { const filePath = path.join( process.cwd(), "public", "uploads", - ...params.path + ...filePathSegments ); const data = await readFile(filePath); @@ -35,7 +36,7 @@ export async function GET( }, }); } catch (error) { - console.warn("[uploads-route] File not found", params.path, error); + console.warn("[uploads-route] File not found", filePathSegments, error); return NextResponse.json({ error: "File not found" }, { status: 404 }); } } diff --git a/src/components/quiz/editor/QuestionEditorDialog.tsx b/src/components/quiz/editor/QuestionEditorDialog.tsx index 1c5eed2..22203d4 100644 --- a/src/components/quiz/editor/QuestionEditorDialog.tsx +++ b/src/components/quiz/editor/QuestionEditorDialog.tsx @@ -26,7 +26,6 @@ import { DialogDescription, DialogHeader, DialogTitle, - DialogClose, } from "@/components/ui/dialog"; import { Tabs, @@ -234,8 +233,21 @@ export function QuestionEditorDialog({ hasValidQuestion && hasEnoughAnswers && hasCorrectAnswer && hasRequiredHint; return ( - - + + + {/* Custom close button positioned at top-right, above sticky header */} + + {/* Sticky Header */}
@@ -247,10 +259,6 @@ export function QuestionEditorDialog({ Create a multiple choice question with 2-6 answer options. - - - Close -
@@ -854,7 +862,11 @@ export function QuestionEditorDialog({ {/* Footer */}
- + {/* Sticky Header */}
@@ -134,10 +146,6 @@ export function SectionEditorDialog({ Sections help organize your quiz into logical groups. - - - Close -
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index f38593b..b350a67 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -33,7 +33,7 @@ const DialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( - + Close - + )) DialogContent.displayName = DialogPrimitive.Content.displayName diff --git a/src/lib/db.ts b/src/lib/db.ts index ef41cd7..d19e1cb 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,9 +1,17 @@ +import "dotenv/config"; import { PrismaClient } from "@prisma/client"; +import { PrismaLibSql } from "@prisma/adapter-libsql"; const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined; }; -export const prisma = globalForPrisma.prisma ?? new PrismaClient(); +function createPrismaClient() { + const databaseUrl = process.env.DATABASE_URL ?? "file:./prisma/data/quiz.db"; + const adapter = new PrismaLibSql({ url: databaseUrl }); + return new PrismaClient({ adapter }); +} + +export const prisma = globalForPrisma.prisma ?? createPrismaClient(); if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; diff --git a/src/middleware.ts b/src/proxy.ts similarity index 98% rename from src/middleware.ts rename to src/proxy.ts index 4489b25..c6fd04f 100644 --- a/src/middleware.ts +++ b/src/proxy.ts @@ -54,7 +54,7 @@ function isLocalhostOnlyRoute(pathname: string): boolean { return LOCALHOST_ONLY_PATTERNS.some((pattern) => pathname.startsWith(pattern)); } -export function middleware(request: NextRequest) { +export function proxy(request: NextRequest) { const { pathname } = request.nextUrl; // Allow all static files and Next.js internals diff --git a/src/server/game-manager.ts b/src/server/game-manager.ts index 9619b9e..31e37c7 100644 --- a/src/server/game-manager.ts +++ b/src/server/game-manager.ts @@ -1,5 +1,4 @@ import { Server, Socket } from "socket.io"; -import { PrismaClient } from "@prisma/client"; import { GameState, PlayerInfo, @@ -25,8 +24,7 @@ import { generateAvatarColor, } from "@/lib/scoring"; import { parseTheme } from "@/lib/theme"; - -const prisma = new PrismaClient(); +import { prisma } from "@/lib/db"; interface ActiveGame { gameCode: string; diff --git a/tailwind.config.ts b/tailwind.config.ts deleted file mode 100644 index 9ed1e30..0000000 --- a/tailwind.config.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { Config } from "tailwindcss"; - -export default { - darkMode: ["class"], - content: [ - "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", - "./src/components/**/*.{js,ts,jsx,tsx,mdx}", - "./src/app/**/*.{js,ts,jsx,tsx,mdx}", - ], - theme: { - extend: { - colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - }, - fontFamily: { - sans: [ - "-apple-system", - "BlinkMacSystemFont", - "Segoe UI", - "Roboto", - "Helvetica Neue", - "Arial", - "sans-serif", - ], - }, - borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - }, - }, - }, - plugins: [require("tailwindcss-animate")], -} satisfies Config; diff --git a/tsconfig.json b/tsconfig.json index 7b28589..877b650 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -10,7 +14,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "plugins": [ { @@ -18,9 +22,20 @@ } ], "paths": { - "@/*": ["./src/*"] - } + "@/*": [ + "./src/*" + ] + }, + "target": "ES2017" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] }