diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8c0a69b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +.git +.env +.gitignore +README.md +*.md diff --git a/.env b/.env deleted file mode 100644 index 84cf550..0000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -VITE_TMDB_TOKEN=eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI4Njk3OWFmMjBlMTJmOWExMjc1OWU4MDJlZTYyYzU0NCIsIm5iZiI6MTc4MzIwNTkxNi45NzUwMDAxLCJzdWIiOiI2YTQ5OTAxYzU3ODRmNzFjMDhlYzlmODYiLCJzY29wZXMiOlsiYXBpX3JlYWQiXSwidmVyc2lvbiI6MX0.vmP2hDHnoCoalZ6ViPvOVc4v91oxVg-r2CqAVpzx34E \ No newline at end of file diff --git a/.env.example b/.env.example index 2d0feab..2ebc54e 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,7 @@ VITE_TMDB_TOKEN=your_tmdb_api_read_access_token_here # Supabase (required for backend) VITE_SUPABASE_URL=https://your-project.supabase.co VITE_SUPABASE_ANON_KEY=your_supabase_anon_key_here +VITE_SUPABASE_SERVICE_KEY=your_supabase_service_role_key_here # Paystack (required for payments) VITE_PAYSTACK_PUBLIC_KEY=your_paystack_public_key_here diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..56fdf6e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - run: npm ci + + - run: npm run lint + + - run: npm run test + + - run: npm run build + + deploy: + if: github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: amondnet/vercel-action@v25 + with: + vercel-token: ${{ secrets.VERCEL_TOKEN }} + vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} + vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} + vercel-args: '--prod' diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..eb587af --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM node:22-alpine AS build +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM nginx:1.27-alpine +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index d678a0c..52e99c6 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Discover, buy, and sell your favorite entertainment media in one beautiful dark- | **State** | React Context (Cart, Wishlist, Auth) | | **Payments** | Paystack inline popup | | **Database** | Supabase (PostgreSQL) | -| **Data Sources** | TMDB API (movies), Open Library (books), Jikan API (manga), Comic Vine (comics) | +| **Data Sources** | TMDB API (movies), Google Books API (books), Jikan API (manga), Open Library (comics) | | **Build** | Vite 8 (Rolldown) | | **Auth** | Supabase Auth (email/password + Google OAuth) | diff --git a/index.html b/index.html index 952d53f..1c078d0 100644 --- a/index.html +++ b/index.html @@ -3,8 +3,16 @@ + - cineverse + + + + + + + + CineVerse — Movies, Books, Manga & Comics
diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..db6c0d2 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,14 @@ +server { + listen 80; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff2?)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/package-lock.json b/package-lock.json index 7367a38..27dd6e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@base-ui/react": "^1.6.0", "@fontsource-variable/geist": "^5.2.9", + "@hookform/resolvers": "^5.4.0", "@supabase/supabase-js": "^2.110.5", "@tailwindcss/vite": "^4.3.2", "@tanstack/react-query": "^5.101.2", @@ -17,8 +18,10 @@ "chart.js": "^4.5.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dotenv": "^16.4.7", "framer-motion": "^12.42.2", "lucide-react": "^1.23.0", + "prop-types": "^15.8.1", "react": "^19.2.7", "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.7", @@ -28,21 +31,83 @@ "react-router-dom": "^7.18.1", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.2", - "tw-animate-css": "^1.4.0" + "tw-animate-css": "^1.4.0", + "zod": "^4.4.3" }, "devDependencies": { "@eslint/js": "^10.0.1", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@vitejs/plugin-react": "^6.0.3", "eslint": "^10.6.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.3", "globals": "^17.7.0", + "jsdom": "^29.1.1", "shadcn": "^4.13.0", - "vite": "^8.1.1" + "vite": "^8.1.1", + "vitest": "^4.1.10" } }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -545,6 +610,159 @@ } } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@dotenvx/dotenvx": { "version": "1.75.1", "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.75.1.tgz", @@ -596,6 +814,19 @@ "node": ">=8" } }, + "node_modules/@dotenvx/dotenvx/node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/@dotenvx/dotenvx/node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -925,6 +1156,24 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@floating-ui/core": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", @@ -985,6 +1234,18 @@ "hono": "^4" } }, + "node_modules/@hookform/resolvers": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.4.0.tgz", + "integrity": "sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1518,6 +1779,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@supabase/auth-js": { "version": "2.110.5", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.110.5.tgz", @@ -1897,6 +2171,96 @@ "react": "^18 || ^19" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@ts-morph/common": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", @@ -1919,6 +2283,32 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -1940,26 +2330,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, "node_modules/@types/validate-npm-package-name": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", @@ -1993,6 +2363,119 @@ } } }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -2148,6 +2631,20 @@ "node": ">=8" } }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2155,6 +2652,26 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", @@ -2219,6 +2736,16 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -2405,6 +2932,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -2710,6 +3247,27 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2729,6 +3287,20 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debounce-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", @@ -2762,6 +3334,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/dedent": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", @@ -2856,6 +3435,16 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2875,6 +3464,14 @@ "node": ">=0.3.1" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dot-prop": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", @@ -2892,10 +3489,9 @@ } }, "node_modules/dotenv": { - "version": "17.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", - "dev": true, + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -2976,6 +3572,19 @@ "node": ">=8.6" } }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -3014,6 +3623,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", @@ -3260,6 +3876,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3330,6 +3956,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -3964,6 +4600,19 @@ "node": ">=16.9.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -4071,6 +4720,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -4222,6 +4881,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -4314,7 +4980,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -4340,6 +5005,57 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -4762,6 +5478,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -4781,6 +5509,17 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -4799,6 +5538,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -4910,6 +5656,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -5036,7 +5792,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5065,6 +5820,20 @@ "node": ">= 10" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -5293,6 +6062,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -5341,6 +6123,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5513,6 +6302,22 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, "node_modules/pretty-ms": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", @@ -5553,6 +6358,23 @@ "node": ">=6" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -5727,6 +6549,14 @@ "react": "*" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/react-router": { "version": "7.18.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", @@ -5782,6 +6612,20 @@ "node": ">= 4" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -5946,6 +6790,19 @@ "dev": true, "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -6205,6 +7062,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -6244,6 +7108,13 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -6254,6 +7125,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -6381,6 +7259,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/systeminformation": { "version": "5.31.12", "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.12.tgz", @@ -6444,6 +7342,23 @@ "dev": true, "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -6460,6 +7375,36 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz", + "integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.9" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz", + "integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6483,6 +7428,32 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-morph": { "version": "26.0.0", "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz", @@ -6794,6 +7765,144 @@ } } }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6810,6 +7919,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -6844,6 +7970,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -6897,7 +8040,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 494061c..1143541 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,14 @@ "dev": "vite", "build": "vite build", "lint": "eslint .", + "test": "vitest run", + "test:watch": "vitest", "preview": "vite preview" }, "dependencies": { "@base-ui/react": "^1.6.0", "@fontsource-variable/geist": "^5.2.9", + "@hookform/resolvers": "^5.4.0", "@supabase/supabase-js": "^2.110.5", "@tailwindcss/vite": "^4.3.2", "@tanstack/react-query": "^5.101.2", @@ -21,6 +24,7 @@ "clsx": "^2.1.1", "framer-motion": "^12.42.2", "lucide-react": "^1.23.0", + "prop-types": "^15.8.1", "react": "^19.2.7", "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.7", @@ -30,18 +34,23 @@ "react-router-dom": "^7.18.1", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.2", - "tw-animate-css": "^1.4.0" + "tw-animate-css": "^1.4.0", + "zod": "^4.4.3", + "dotenv": "^16.4.7" }, "devDependencies": { "@eslint/js": "^10.0.1", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@vitejs/plugin-react": "^6.0.3", "eslint": "^10.6.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.3", "globals": "^17.7.0", + "jsdom": "^29.1.1", "shadcn": "^4.13.0", - "vite": "^8.1.1" + "vite": "^8.1.1", + "vitest": "^4.1.10" } } diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..9299bd3 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..a15499f --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,16 @@ +{ + "name": "CineVerse", + "short_name": "CineVerse", + "description": "E-commerce marketplace for movies, books, manga, and comics", + "start_url": "/", + "display": "standalone", + "background_color": "#0f172a", + "theme_color": "#8b5cf6", + "icons": [ + { + "src": "/favicon.svg", + "sizes": "any", + "type": "image/svg+xml" + } + ] +} diff --git a/public/placeholder-book.svg b/public/placeholder-book.svg new file mode 100644 index 0000000..ccffb5b --- /dev/null +++ b/public/placeholder-book.svg @@ -0,0 +1,11 @@ + + + + + + + + + + No Image Available + diff --git a/public/placeholder-comic.svg b/public/placeholder-comic.svg new file mode 100644 index 0000000..30969ca --- /dev/null +++ b/public/placeholder-comic.svg @@ -0,0 +1,11 @@ + + + + + + + + + + No Image Available + diff --git a/public/placeholder-manga.svg b/public/placeholder-manga.svg new file mode 100644 index 0000000..6e48df3 --- /dev/null +++ b/public/placeholder-manga.svg @@ -0,0 +1,10 @@ + + + + + + + + + No Image Available + diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..b6335a9 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Allow: / +Sitemap: https://cineverse.app/sitemap.xml diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..55bbd97 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,11 @@ + + + https://cineverse.app/1.0 + https://cineverse.app/movies0.8 + https://cineverse.app/books0.8 + https://cineverse.app/manga0.8 + https://cineverse.app/comics0.8 + https://cineverse.app/cart0.5 + https://cineverse.app/wishlist0.5 + https://cineverse.app/search0.6 + diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..23f74e1 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,65 @@ +const CACHE_NAME = 'cineverse-v1' +const STATIC_ASSETS = [ + '/', + '/index.html', + '/manifest.json', + '/favicon.svg', + '/placeholder-book.svg', + '/placeholder-manga.svg', + '/placeholder-comic.svg', +] + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + return cache.addAll(STATIC_ASSETS) + }) + ) +}) + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((keys) => { + return Promise.all( + keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)) + ) + }) + ) +}) + +self.addEventListener('fetch', (event) => { + if (event.request.url.includes('/api/') || event.request.url.includes('supabase')) { + event.respondWith(networkFirst(event.request)) + } else { + event.respondWith(cacheFirst(event.request)) + } +}) + +async function cacheFirst(request) { + const cached = await caches.match(request) + if (cached) return cached + const response = await fetch(request) + if (response.ok) { + const cache = await caches.open(CACHE_NAME) + cache.put(request, response.clone()) + } + return response +} + +async function networkFirst(request) { + try { + const response = await fetch(request) + if (response.ok) { + const cache = await caches.open(CACHE_NAME) + cache.put(request, response.clone()) + } + return response + } catch { + const cached = await caches.match(request) + if (cached) return cached + return new Response(JSON.stringify({ error: 'Offline' }), { + status: 503, + headers: { 'Content-Type': 'application/json' }, + }) + } +} diff --git a/src/assets/animations b/src/assets/animations deleted file mode 100644 index e69de29..0000000 diff --git a/src/assets/images/hero-bg.webp b/src/assets/images/hero-bg.webp new file mode 100644 index 0000000..9040f1d Binary files /dev/null and b/src/assets/images/hero-bg.webp differ diff --git a/src/components/Books/BookCard.jsx b/src/components/Books/BookCard.jsx index b9fa478..3e5e5f7 100644 --- a/src/components/Books/BookCard.jsx +++ b/src/components/Books/BookCard.jsx @@ -1,3 +1,4 @@ +import { memo } from 'react' import { Link } from 'react-router-dom' import { motion } from 'framer-motion' import { FaHeart, FaShoppingCart, FaStar } from 'react-icons/fa' @@ -5,6 +6,7 @@ import { ROUTES } from '@/constants/routes' import { useCart } from '@/context/CartContext' import { useWishlist } from '@/context/WishlistContext' import { formatCurrency } from '@/utils/formatCurrency' +import PropTypes from 'prop-types' import toast from 'react-hot-toast' function BookCard({ book, index = 0 }) { @@ -77,4 +79,16 @@ function BookCard({ book, index = 0 }) { ) } -export default BookCard +BookCard.propTypes = { + book: PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + title: PropTypes.string.isRequired, + image: PropTypes.string, + price: PropTypes.number, + authors: PropTypes.arrayOf(PropTypes.string), + averageRating: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + }).isRequired, + index: PropTypes.number, +} + +export default memo(BookCard) diff --git a/src/components/Categories/CategoryCard.jsx b/src/components/Categories/CategoryCard.jsx index ea8a50d..a0c5cc5 100644 --- a/src/components/Categories/CategoryCard.jsx +++ b/src/components/Categories/CategoryCard.jsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom' import { motion } from 'framer-motion' +import PropTypes from 'prop-types' function CategoryCard({ category, index = 0 }) { const IconComponent = category.icon @@ -65,4 +66,14 @@ function CategoryCard({ category, index = 0 }) { ) } +CategoryCard.propTypes = { + category: PropTypes.shape({ + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + icon: PropTypes.elementType.isRequired, + count: PropTypes.number, + }).isRequired, + index: PropTypes.number, +} + export default CategoryCard diff --git a/src/components/Comics/ComicCard.jsx b/src/components/Comics/ComicCard.jsx index d457e29..6a9f32e 100644 --- a/src/components/Comics/ComicCard.jsx +++ b/src/components/Comics/ComicCard.jsx @@ -1,3 +1,4 @@ +import { memo } from 'react' import { Link } from 'react-router-dom' import { motion } from 'framer-motion' import { FaHeart, FaShoppingCart } from 'react-icons/fa' @@ -5,6 +6,7 @@ import { ROUTES } from '@/constants/routes' import { useCart } from '@/context/CartContext' import { useWishlist } from '@/context/WishlistContext' import { formatCurrency } from '@/utils/formatCurrency' +import PropTypes from 'prop-types' import toast from 'react-hot-toast' function ComicCard({ comic, index = 0 }) { @@ -76,4 +78,16 @@ function ComicCard({ comic, index = 0 }) { ) } -export default ComicCard +ComicCard.propTypes = { + comic: PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + title: PropTypes.string.isRequired, + image: PropTypes.string, + price: PropTypes.number, + authors: PropTypes.arrayOf(PropTypes.string), + firstPublishYear: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + }).isRequired, + index: PropTypes.number, +} + +export default memo(ComicCard) diff --git a/src/components/ErrorBoundary.jsx b/src/components/ErrorBoundary.jsx index 28b57f2..ab207c5 100644 --- a/src/components/ErrorBoundary.jsx +++ b/src/components/ErrorBoundary.jsx @@ -1,4 +1,5 @@ import { Component } from 'react' +import PropTypes from 'prop-types' class ErrorBoundary extends Component { constructor(props) { @@ -14,6 +15,10 @@ class ErrorBoundary extends Component { console.error('ErrorBoundary caught:', error, errorInfo) } + static propTypes = { + children: PropTypes.node, + } + render() { if (this.state.hasError) { return ( diff --git a/src/components/Hero/HeroBackground.jsx b/src/components/Hero/HeroBackground.jsx index 013bf50..36e271e 100644 --- a/src/components/Hero/HeroBackground.jsx +++ b/src/components/Hero/HeroBackground.jsx @@ -1,4 +1,4 @@ -import Herobg from "@/assets/images/hero-bg.png"; +import Herobg from "@/assets/images/hero-bg.webp"; function HeroBackground() { return ( @@ -6,7 +6,7 @@ function HeroBackground() {
diff --git a/src/components/Hero/HeroFloatingCards.jsx b/src/components/Hero/HeroFloatingCards.jsx index 57f1cc0..f245fbd 100644 --- a/src/components/Hero/HeroFloatingCards.jsx +++ b/src/components/Hero/HeroFloatingCards.jsx @@ -1,9 +1,70 @@ +import { motion } from "framer-motion"; +import { FaFilm, FaBook, FaDragon, FaMasksTheater } from "react-icons/fa6"; + +const floatingCards = [ + { + icon: FaFilm, + title: "Movies", + gradient: "from-red-500 to-orange-500", + x: 0, + y: -20, + delay: 0, + }, + { + icon: FaBook, + title: "Books", + gradient: "from-violet-500 to-fuchsia-500", + x: 100, + y: 40, + delay: 0.3, + }, + { + icon: FaDragon, + title: "Manga", + gradient: "from-pink-500 to-rose-500", + x: -80, + y: 60, + delay: 0.6, + }, + { + icon: FaMasksTheater, + title: "Comics", + gradient: "from-emerald-500 to-teal-500", + x: 60, + y: -50, + delay: 0.9, + }, +]; + function HeroFloatingCards() { return ( -
- {/* Coming in the next phase */} +
+ {floatingCards.map((card) => ( + + +
+ +
+
+

{card.title}

+

Explore now

+
+
+
+ ))}
); } -export default HeroFloatingCards; \ No newline at end of file +export default HeroFloatingCards; diff --git a/src/components/Manga/MangaCard.jsx b/src/components/Manga/MangaCard.jsx index f9f4c85..1ec0012 100644 --- a/src/components/Manga/MangaCard.jsx +++ b/src/components/Manga/MangaCard.jsx @@ -1,3 +1,4 @@ +import { memo } from 'react' import { Link } from 'react-router-dom' import { motion } from 'framer-motion' import { FaHeart, FaShoppingCart, FaStar } from 'react-icons/fa' @@ -5,6 +6,7 @@ import { ROUTES } from '@/constants/routes' import { useCart } from '@/context/CartContext' import { useWishlist } from '@/context/WishlistContext' import { formatCurrency } from '@/utils/formatCurrency' +import PropTypes from 'prop-types' import toast from 'react-hot-toast' function MangaCard({ manga, index = 0 }) { @@ -83,4 +85,18 @@ function MangaCard({ manga, index = 0 }) { ) } -export default MangaCard +MangaCard.propTypes = { + manga: PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + title: PropTypes.string.isRequired, + image: PropTypes.string, + price: PropTypes.number, + authors: PropTypes.arrayOf(PropTypes.string), + score: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + type: PropTypes.string, + chapters: PropTypes.number, + }).isRequired, + index: PropTypes.number, +} + +export default memo(MangaCard) diff --git a/src/components/Movies/MovieCard.jsx b/src/components/Movies/MovieCard.jsx index f1c24ca..b35f546 100644 --- a/src/components/Movies/MovieCard.jsx +++ b/src/components/Movies/MovieCard.jsx @@ -6,6 +6,8 @@ import { ROUTES } from "@/constants/routes"; import { useCart } from "@/context/CartContext"; import { useWishlist } from "@/context/WishlistContext"; import { formatCurrency } from "@/utils/formatCurrency"; +import { memo } from "react"; +import PropTypes from "prop-types"; import toast from "react-hot-toast"; function MovieCard({ movie, index = 0 }) { @@ -94,4 +96,16 @@ function MovieCard({ movie, index = 0 }) { ); } -export default MovieCard; +MovieCard.propTypes = { + movie: PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + title: PropTypes.string.isRequired, + poster_path: PropTypes.string, + vote_average: PropTypes.number, + release_date: PropTypes.string, + price: PropTypes.number, + }).isRequired, + index: PropTypes.number, +} + +export default memo(MovieCard); diff --git a/src/components/Movies/MovieCast.jsx b/src/components/Movies/MovieCast.jsx index 9716b3b..114cb22 100644 --- a/src/components/Movies/MovieCast.jsx +++ b/src/components/Movies/MovieCast.jsx @@ -1,35 +1,29 @@ import { IMAGE_BASE_URL } from "@/constants/tmdb"; +import PropTypes from 'prop-types'; function MovieCast({ cast }) { if (!cast || cast.length === 0) return null; return ( -
-

- Top Billed Cast +
+

+ Top Billed{" "} + Cast

- - {/* Smooth horizontal scroll container */} +
{cast.slice(0, 10).map((member) => ( -
- {/* Cast Profile Image */} -
+
{member.name}
- - {/* Names & Characters */}

{member.name}

@@ -43,4 +37,15 @@ function MovieCast({ cast }) { ); } -export default MovieCast; \ No newline at end of file +MovieCast.propTypes = { + cast: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number.isRequired, + name: PropTypes.string, + character: PropTypes.string, + profile_path: PropTypes.string, + }) + ), +}; + +export default MovieCast; diff --git a/src/components/Movies/MovieGallery.jsx b/src/components/Movies/MovieGallery.jsx index 6d4297a..4fa7471 100644 --- a/src/components/Movies/MovieGallery.jsx +++ b/src/components/Movies/MovieGallery.jsx @@ -1,46 +1,34 @@ import { IMAGE_BASE_URL } from "@/constants/tmdb"; import { motion } from "framer-motion"; +import PropTypes from 'prop-types'; function MovieGallery({ images }) { if (!images?.backdrops?.length) return null; return ( -
-
+
+

- Gallery + Movie{" "} + Gallery

- -

- Official movie stills -

+

Official movie stills

- {images.backdrops.slice(0, 6).map((image) => ( + {images.backdrops.slice(0, 6).map((image, i) => ( Movie still ))} @@ -49,4 +37,19 @@ function MovieGallery({ images }) { ); } -export default MovieGallery; \ No newline at end of file +MovieGallery.propTypes = { + images: PropTypes.shape({ + backdrops: PropTypes.arrayOf( + PropTypes.shape({ + file_path: PropTypes.string, + }) + ), + posters: PropTypes.arrayOf( + PropTypes.shape({ + file_path: PropTypes.string, + }) + ), + }), +}; + +export default MovieGallery; diff --git a/src/components/Movies/MovieHero.jsx b/src/components/Movies/MovieHero.jsx index bc9424e..6dc6087 100644 --- a/src/components/Movies/MovieHero.jsx +++ b/src/components/Movies/MovieHero.jsx @@ -1,107 +1,87 @@ import { FaStar, FaHeart, FaShoppingCart } from "react-icons/fa"; import { BACKDROP_BASE_URL, IMAGE_BASE_URL } from "@/constants/tmdb"; import heroBg from "@/assets/images/hero-bg.png"; +import PropTypes from 'prop-types'; function MovieHero({ movie }) { return (
- {/* Background */} {movie.title} { - e.target.src = heroBg; - }} + onError={(e) => { e.target.src = heroBg; }} /> +
+
+
- {/* Overlay */} -
- - {/* Gradient */} -
- - {/* Content */}
- - {/* Poster */} {movie.title} - {/* Movie Info */}
- -

+

{movie.title}

-
- - - +
+ + {movie.vote_average.toFixed(1)} - - - {movie.release_date?.split("-")[0]} - - - - {movie.runtime} min - - + {movie.release_date?.split("-")[0]} + {movie.runtime} min
- {/* Genres */} -
- +
{movie.genres?.map((genre) => ( - + {genre.name} ))} -
- {/* Overview */} -

+

{movie.overview}

- {/* Buttons */}
- - - - -
-
-
); } -export default MovieHero; \ No newline at end of file +MovieHero.propTypes = { + movie: PropTypes.shape({ + title: PropTypes.string.isRequired, + backdrop_path: PropTypes.string, + poster_path: PropTypes.string, + vote_average: PropTypes.number.isRequired, + release_date: PropTypes.string, + runtime: PropTypes.number, + overview: PropTypes.string, + genres: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number, + name: PropTypes.string, + }) + ), + }), +}; + +export default MovieHero; diff --git a/src/components/Movies/MovieInfo.jsx b/src/components/Movies/MovieInfo.jsx index 2b85b8f..1a4bf6b 100644 --- a/src/components/Movies/MovieInfo.jsx +++ b/src/components/Movies/MovieInfo.jsx @@ -1,25 +1,21 @@ import { FaPlay } from "react-icons/fa"; +import PropTypes from 'prop-types'; function MovieInfo({ movie, videos }) { const trailer = videos?.find( - (video) => - video.site === "YouTube" && - video.type === "Trailer" + (video) => video.site === "YouTube" && video.type === "Trailer" ); return (
-
- - {/* Left */}
- -

- About this movie +

+ About{" "} + this movie

-

+

{movie.overview}

@@ -28,101 +24,78 @@ function MovieInfo({ movie, videos }) { href={`https://www.youtube.com/watch?v=${trailer.key}`} target="_blank" rel="noopener noreferrer" - className="mt-8 inline-flex items-center gap-3 rounded-xl bg-red-600 px-6 py-3 font-semibold transition hover:bg-red-700" + className="group mt-8 inline-flex items-center gap-3 rounded-xl bg-gradient-to-r from-red-600 to-rose-600 px-6 py-3 font-semibold text-white shadow-lg shadow-red-500/25 hover:from-red-500 hover:to-rose-500 transition-all duration-300" > - + Watch Trailer )} -
- {/* Right */} -
- +
-

- Original Language -

- -

- {movie.original_language.toUpperCase()} -

+

Original Language

+

{movie.original_language.toUpperCase()}

-
-

- Status -

- -

- {movie.status} -

+

Status

+

{movie.status}

-
-

- Runtime -

- -

- {movie.runtime} mins -

+

Runtime

+

{movie.runtime} mins

-
-

- Budget -

- -

- ${movie.budget.toLocaleString()} -

+

Budget

+

${movie.budget.toLocaleString()}

-
-

- Revenue -

- -

- ${movie.revenue.toLocaleString()} -

+

Revenue

+

${movie.revenue.toLocaleString()}

-
-

- Genres -

- +

Genres

{movie.genres?.map((genre) => ( - + {genre.name} ))}
-
-

- Rating -

- -

- ⭐ {movie.vote_average.toFixed(1)} / 10 -

+

Rating

+

⭐ {movie.vote_average.toFixed(1)} / 10

-
-
-
); } -export default MovieInfo; \ No newline at end of file +MovieInfo.propTypes = { + movie: PropTypes.shape({ + overview: PropTypes.string, + original_language: PropTypes.string, + status: PropTypes.string, + runtime: PropTypes.number, + budget: PropTypes.number, + revenue: PropTypes.number, + genres: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number, + name: PropTypes.string, + }) + ), + vote_average: PropTypes.number, + }), + videos: PropTypes.arrayOf( + PropTypes.shape({ + site: PropTypes.string, + type: PropTypes.string, + key: PropTypes.string, + }) + ), +}; + +export default MovieInfo; diff --git a/src/components/Movies/MovieReviews.jsx b/src/components/Movies/MovieReviews.jsx index 2262639..bb79c8a 100644 --- a/src/components/Movies/MovieReviews.jsx +++ b/src/components/Movies/MovieReviews.jsx @@ -1,5 +1,6 @@ import { motion } from "framer-motion"; import { FaStar, FaUserCircle } from "react-icons/fa"; +import PropTypes from 'prop-types'; function MovieReviews({ reviews }) { if (!reviews?.length) return null; @@ -75,4 +76,18 @@ function MovieReviews({ reviews }) { ); } +MovieReviews.propTypes = { + reviews: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + author: PropTypes.string, + content: PropTypes.string, + created_at: PropTypes.string, + author_details: PropTypes.shape({ + rating: PropTypes.number, + }), + }) + ), +}; + export default MovieReviews; \ No newline at end of file diff --git a/src/components/Movies/MovieSkeleton.jsx b/src/components/Movies/MovieSkeleton.jsx index acea49a..782f568 100644 --- a/src/components/Movies/MovieSkeleton.jsx +++ b/src/components/Movies/MovieSkeleton.jsx @@ -11,4 +11,6 @@ function MovieSkeleton() { ); } +MovieSkeleton.propTypes = {}; + export default MovieSkeleton; diff --git a/src/components/Movies/MovieTrailer.jsx b/src/components/Movies/MovieTrailer.jsx index 5059eca..8e3c0c9 100644 --- a/src/components/Movies/MovieTrailer.jsx +++ b/src/components/Movies/MovieTrailer.jsx @@ -8,6 +8,7 @@ import { motion } from "framer-motion"; import { FaPlay, FaStar, FaClock, FaCalendarAlt } from "react-icons/fa"; import { BACKDROP_BASE_URL } from "@/constants/tmdb"; +import PropTypes from 'prop-types'; function MovieTrailer({ movie, videos }) { const trailer = videos?.find( @@ -191,4 +192,22 @@ function MovieTrailer({ movie, videos }) { ); } +MovieTrailer.propTypes = { + movie: PropTypes.shape({ + backdrop_path: PropTypes.string, + title: PropTypes.string, + vote_average: PropTypes.number, + runtime: PropTypes.number, + release_date: PropTypes.string, + }), + videos: PropTypes.arrayOf( + PropTypes.shape({ + site: PropTypes.string, + type: PropTypes.string, + key: PropTypes.string, + name: PropTypes.string, + }) + ), +}; + export default MovieTrailer; \ No newline at end of file diff --git a/src/components/Movies/SimilarMovies.jsx b/src/components/Movies/SimilarMovies.jsx index 5d4a1f2..365871f 100644 --- a/src/components/Movies/SimilarMovies.jsx +++ b/src/components/Movies/SimilarMovies.jsx @@ -1,46 +1,57 @@ +import { motion } from "framer-motion"; import { IMAGE_BASE_URL } from "@/constants/tmdb"; import { ROUTES } from "@/constants/routes"; import { Link } from "react-router-dom"; +import PropTypes from 'prop-types'; function SimilarMovies({ movies }) { if (!movies || movies.length === 0) return null; return ( -
-

- Similar Movies You Might Like +
+

+ Similar{" "} + Movies

- - {/* Responsive grid for movie cards */} +
- {movies.slice(0, 5).map((simMovie) => ( - ( + - {/* Poster Wrapper with Zoom effect */} -
- {simMovie.title} -
- - {/* Movie Title */} -

- {simMovie.title} -

- + +
+
+
+ {simMovie.title} +
+
+

+ {simMovie.title} +

+ + ))}
); } -export default SimilarMovies; \ No newline at end of file +SimilarMovies.propTypes = { + movies: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number.isRequired, + title: PropTypes.string, + poster_path: PropTypes.string, + }) + ), +}; + +export default SimilarMovies; diff --git a/src/components/Movies/WatchProviders.jsx b/src/components/Movies/WatchProviders.jsx index ca28eca..bc7a973 100644 --- a/src/components/Movies/WatchProviders.jsx +++ b/src/components/Movies/WatchProviders.jsx @@ -1,4 +1,5 @@ import { IMAGE_BASE_URL } from "@/constants/tmdb"; +import PropTypes from 'prop-types'; function WatchProviders({ providers }) { const nigeria = providers?.NG || providers?.US; @@ -6,41 +7,27 @@ function WatchProviders({ providers }) { if (!nigeria?.flatrate?.length) return null; return ( -
-
+
+

- Where to Watch + Where to{" "} + Watch

- - - Available on - +

Available on

{nigeria.flatrate.map((provider) => (
{provider.provider_name} - - - {provider.provider_name} - + {provider.provider_name}
))}
@@ -48,4 +35,27 @@ function WatchProviders({ providers }) { ); } -export default WatchProviders; \ No newline at end of file +WatchProviders.propTypes = { + providers: PropTypes.shape({ + NG: PropTypes.shape({ + flatrate: PropTypes.arrayOf( + PropTypes.shape({ + provider_id: PropTypes.number, + logo_path: PropTypes.string, + provider_name: PropTypes.string, + }) + ), + }), + US: PropTypes.shape({ + flatrate: PropTypes.arrayOf( + PropTypes.shape({ + provider_id: PropTypes.number, + logo_path: PropTypes.string, + provider_name: PropTypes.string, + }) + ), + }), + }), +}; + +export default WatchProviders; diff --git a/src/components/ProtectedRoute.jsx b/src/components/ProtectedRoute.jsx index 4935d06..ccd3908 100644 --- a/src/components/ProtectedRoute.jsx +++ b/src/components/ProtectedRoute.jsx @@ -1,6 +1,7 @@ import { Navigate, useLocation } from 'react-router-dom' import { useAuth } from '@/context/AuthContext' import { ROUTES } from '@/constants/routes' +import PropTypes from 'prop-types' export function ProtectedRoute({ children, roles }) { const { isAuthenticated, role, loading } = useAuth() @@ -24,3 +25,8 @@ export function ProtectedRoute({ children, roles }) { return children } + +ProtectedRoute.propTypes = { + children: PropTypes.node, + roles: PropTypes.arrayOf(PropTypes.string), +} diff --git a/src/components/Reviews/ReviewForm.jsx b/src/components/Reviews/ReviewForm.jsx index 6c89638..b73911d 100644 --- a/src/components/Reviews/ReviewForm.jsx +++ b/src/components/Reviews/ReviewForm.jsx @@ -1,15 +1,25 @@ import { useState } from 'react' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { z } from 'zod' import { FaStar } from 'react-icons/fa' import { useAuth } from '@/context/AuthContext' import { supabase } from '@/lib/supabase' import toast from 'react-hot-toast' +import PropTypes from 'prop-types' + +const reviewSchema = z.object({ + comment: z.string().min(1, 'Please write a comment'), +}) function ReviewForm({ productId, onReviewAdded }) { const { user } = useAuth() const [rating, setRating] = useState(0) const [hover, setHover] = useState(0) - const [comment, setComment] = useState('') - const [submitting, setSubmitting] = useState(false) + + const { register, handleSubmit, reset, formState: { errors, isSubmitting } } = useForm({ + resolver: zodResolver(reviewSchema), + }) if (!user) { return ( @@ -19,12 +29,9 @@ function ReviewForm({ productId, onReviewAdded }) { ) } - const handleSubmit = async (e) => { - e.preventDefault() + const onSubmit = async ({ comment }) => { if (rating === 0) { toast.error('Please select a rating'); return } - if (!comment.trim()) { toast.error('Please write a comment'); return } - setSubmitting(true) const { error } = await supabase.from('reviews').insert({ user_id: user.id, product_id: productId, @@ -32,8 +39,6 @@ function ReviewForm({ productId, onReviewAdded }) { comment: comment.trim(), }).select().single() - setSubmitting(false) - if (error) { if (error.code === '23505') { toast.error('You already reviewed this product') @@ -45,13 +50,13 @@ function ReviewForm({ productId, onReviewAdded }) { toast.success('Review submitted!') setRating(0) - setComment('') + reset() onReviewAdded?.() } return ( -
-

Write a Review

+ +

Write a Review

{[1, 2, 3, 4, 5].map((star) => ( @@ -75,22 +80,27 @@ function ReviewForm({ productId, onReviewAdded }) {