diff --git a/.gitignore b/.gitignore index 4791e93..729b260 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,7 @@ dist-ssr *.sln *.sw? .vs/* + +#next +.next +next-env.d.ts \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index 3867a0f..0000000 --- a/.husky/pre-commit +++ /dev/null @@ -1 +0,0 @@ -npm run lint diff --git a/.husky/pre-push b/.husky/pre-push deleted file mode 100644 index bf0001c..0000000 --- a/.husky/pre-push +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -echo "Running pre-push hook..." -npm test \ No newline at end of file diff --git a/README.md b/README.md index adf466b..49b826d 100644 --- a/README.md +++ b/README.md @@ -316,3 +316,272 @@ git checkout -b app-state-management ``` + +
+ +## TASK 4: Next.js. Server Side Rendering + + +Task: https://github.com/rolling-scopes-school/tasks/blob/master/react/modules/tasks/nextjs-ssr-ssg.md + +Doc: https://nextjs.org/docs/app/building-your-application/upgrading/from-vite + +## Why Switch? + +There are several reasons why you might want to switch from Vite to Next.js: + +### Data Fetching Strategy + +With Next.js, you can choose your data fetching strategy per page or component. You can fetch data at build time, on the server at request time, or on the client. For example, you can fetch CMS data and render blog posts at build time, which will be efficiently cached on a CDN. + +### Middleware + +Next.js Middleware allows you to run code on the server before a request is completed. This can be helpful for features like user authentication or internationalization. + +### Built-in Optimizations + +Next.js comes with built-in optimizations for images, fonts, and third-party scripts, helping improve application performance. + +--- + +## Migration Steps + +The goal of this migration is to transition your app to Next.js as smoothly as possible, starting with a purely client-side application (SPA). We'll also avoid migrating your existing router initially to minimize issues and reduce merge conflicts. + +### Step 1: Install the Next.js Dependency + +First, install Next.js as a dependency: + +```bash +npm install next@latest +``` + +### Step 2: Create the Next.js Configuration File + +Create a `next.config.mjs` file at the root of your project: + +```javascript +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'export', // Outputs a Single-Page Application (SPA). + distDir: './dist', // Changes the build output directory to `./dist/`. +}; + +export default nextConfig; +``` + +### Step 3: Update TypeScript Configuration + +If you're using TypeScript, update your `tsconfig.json` file with the following changes to make it compatible with Next.js. If not, you can skip this step. + +- Remove the project reference to `tsconfig.node.json` +- Add `./dist/types/**/*.ts` and `./next-env.d.ts` to the `include` array +- Add `./node_modules` to the `exclude` array +- Add `{ "name": "next" }` to the `plugins` array +- Set `esModuleInterop` to `true` +- Set `jsx` to `preserve` +- Set `allowJs` to `true` +- Set `forceConsistentCasingInFileNames` to `true` +- Set `incremental` to `true` + +Example `tsconfig.json`: + +```json +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "incremental": true, + "plugins": [{ "name": "next" }] + }, + "include": ["./src", "./dist/types/**/*.ts", "./next-env.d.ts"], + "exclude": ["./node_modules"] +} +``` + +### Step 4: Create the Root Layout + +Next.js requires a root layout file, which is a React Server Component that wraps all pages. This is similar to `index.html` in Vite. + +Create the following files: + +1. **`src/app/layout.tsx`**: + +```tsx +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + My App + + + +
{children}
+ + + ); +} +``` + +2. Move relevant metadata files (e.g., favicon, robots.txt) into the `app` directory and remove `` tags. + +3. Use the `Metadata` API for managing head content: + +```tsx +import type { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'My App', + description: 'My App is a...', +}; +``` + +### Step 5: Create the Entrypoint Page + +In Next.js, the entrypoint is a page declared via `page.tsx`. We will configure a catch-all route. + +1. Create a directory `app/[[...slug]]/` and inside it, create `page.tsx`: + +```tsx +import '../../index.css'; + +export function generateStaticParams() { + return [{ slug: [''] }]; +} + +export default function Page() { + return '...'; +} +``` + +2. Create a `client.tsx` for running the Vite application in Next.js as a client-side app: + +```tsx +'use client'; + +import dynamic from 'next/dynamic'; + +const App = dynamic(() => import('../../App'), { ssr: false }); + +export function ClientOnly() { + return ; +} +``` + +3. Update `page.tsx` to render the client-only component: + +```tsx +import '../../index.css'; +import { ClientOnly } from './client'; + +export function generateStaticParams() { + return [{ slug: [''] }]; +} + +export default function Page() { + return ; +} +``` + +### Step 6: Update Static Image Imports + +Next.js handles static images differently from Vite. Update your image imports: + +1. **Before**: + +```tsx +import image from './img.png'; +``` + +2. **After**: + +```tsx +import image from '../public/img.png'; + +; +``` + +### Step 7: Migrate Environment Variables + +- Change all `VITE_` variables to `NEXT_PUBLIC_`. +- Replace `import.meta.env.MODE` with `process.env.NODE_ENV`. +- Replace `import.meta.env.PROD` with `process.env.NODE_ENV === 'production'`. +- Replace `import.meta.env.SSR` with `typeof window !== 'undefined'`. + +### Step 8: Update Scripts in `package.json` + +Update your `scripts` to use Next.js commands: + +```json +{ + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + } +} +``` + +And update `.gitignore`: + +``` +.next +next-env.d.ts +dist +``` + +### Step 9: Clean Up + +Remove Vite-related files from your project: + +- Delete `main.tsx` +- Delete `index.html` +- Delete `vite-env.d.ts` +- Delete `vite.config.ts` +- Uninstall Vite dependencies + +Removing vite + +```bash +npm uninstall vite @vitejs/plugin-react react-router-dom +``` + +Install SASS + +```bash + npm i sass +``` + +## Next Steps + +Now that your Next.js app is up and running, you can start leveraging additional features: + +- Migrate from React Router to Next.js App Router for automatic code splitting, server-side rendering, and React Server Components. +- Optimize images and fonts with Next.js built-in components. +- Use Next.js's built-in components for third-party scripts optimization. +- Update your ESLint configuration to support Next.js rules. + +This concludes the initial migration process from Vite to Next.js. + +
diff --git a/declarations.d.ts b/declarations.d.ts new file mode 100644 index 0000000..a6f1d44 --- /dev/null +++ b/declarations.d.ts @@ -0,0 +1,9 @@ +declare module '*.png' { + const value: string; + export default value; +} + +declare module '*.scss' { + const content: Record; + export default content; +} diff --git a/index.html b/index.html deleted file mode 100644 index 0030c0b..0000000 --- a/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - React App - - -
- - - diff --git a/jest.config.js b/jest.config.js index b8f2b41..683f544 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,5 @@ export default { - collectCoverageFrom: ['**/*.tsx'], + collectCoverageFrom: ['src/**/*.tsx', 'src/**/*.ts'], coveragePathIgnorePatterns: [ '[/\\\\](node_modules|.next|jest|docker|Deploy|.vscode|public)[/\\\\]', ], diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..895b41b --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,10 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'export', // Outputs a Single-Page Application (SPA). + distDir: './dist', // Changes the build output directory to `./dist/` + images: { + unoptimized: true, + }, +}; + +export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 57daa0d..136c35e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,11 @@ "@reduxjs/toolkit": "^2.5.1", "axios": "^1.7.9", "classnames": "^2.5.1", + "next": "^15.1.7", "react": "^18.3.1", "react-dom": "^18.3.1", "react-redux": "^9.2.0", - "react-router-dom": "^7.1.5" + "sass": "^1.85.1" }, "devDependencies": { "@babel/preset-env": "^7.26.8", @@ -28,7 +29,6 @@ "@types/node": "^22.13.1", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", - "@vitejs/plugin-react-swc": "^3.5.0", "eslint": "^9.17.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.2.3", @@ -41,13 +41,13 @@ "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "jest-fetch-mock": "^3.0.3", "prettier": "3.4.2", "sass-embedded": "^1.83.4", "ts-jest": "^29.2.5", "typescript": "~5.6.2", "typescript-eslint": "^8.18.2", - "util": "^0.12.5", - "vite": "^6.0.5" + "util": "^0.12.5" } }, "node_modules/@adobe/css-tools": { @@ -1963,21 +1963,14 @@ "dev": true, "license": "(Apache-2.0 AND BSD-3-Clause)" }, - "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@emnapi/runtime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@eslint-community/eslint-utils": { @@ -2187,6 +2180,367 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2596,95 +2950,538 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "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" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.7.tgz", + "integrity": "sha512-d9jnRrkuOH7Mhi+LHav2XW91HOgTAWHxjMPkXMGBc9B2b7614P7kjt8tAplRvJpbSt4nbO1lugcT/kAaWzjlLQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.7.tgz", + "integrity": "sha512-hPFwzPJDpA8FGj7IKV3Yf1web3oz2YsR8du4amKw8d+jAOHfYHYFpMkoF6vgSY4W6vB29RtZEklK9ayinGiCmQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.7.tgz", + "integrity": "sha512-2qoas+fO3OQKkU0PBUfwTiw/EYpN+kdAx62cePRyY1LqKtP09Vp5UcUntfZYajop5fDFTjSxCHfZVRxzi+9FYQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.7.tgz", + "integrity": "sha512-sKLLwDX709mPdzxMnRIXLIT9zaX2w0GUlkLYQnKGoXeWUhcvpCrK+yevcwCJPdTdxZEUA0mOXGLdPsGkudGdnA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.7.tgz", + "integrity": "sha512-zblK1OQbQWdC8fxdX4fpsHDw+VSpBPGEUX4PhSE9hkaWPrWoeIJn+baX53vbsbDRaDKd7bBNcXRovY1hEhFd7w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.7.tgz", + "integrity": "sha512-GOzXutxuLvLHFDAPsMP2zDBMl1vfUHHpdNpFGhxu90jEzH6nNIgmtw/s1MDwpTOiM+MT5V8+I1hmVFeAUhkbgQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.7.tgz", + "integrity": "sha512-WrZ7jBhR7ATW1z5iEQ0ZJfE2twCNSXbpCSaAunF3BKcVeHFADSI/AW1y5Xt3DzTqPF1FzQlwQTewqetAABhZRQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.7.tgz", + "integrity": "sha512-LDnj1f3OVbou1BqvvXVqouJZKcwq++mV2F+oFHptToZtScIEnhNRJAhJzqAtTE2dB31qDYL45xJwrc+bLeKM2Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.7.tgz", + "integrity": "sha512-dC01f1quuf97viOfW05/K8XYv2iuBgAxJZl7mbCKEjMgdQl5JjAKJ0D2qMKZCgPWDeFbFT0Q0nYWwytEW0DWTQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.0.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@jridgewell/resolve-uri": { - "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, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.0.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.0.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 8" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 8" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" }, "engines": { - "node": ">= 8" + "node": ">=0.10" } }, "node_modules/@pkgr/core": { @@ -2724,20 +3521,6 @@ } } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.32.1.tgz", - "integrity": "sha512-ar80GhdZb4DgmW3myIS9nRFYcpJRSME8iqWgzH2i44u+IdrzmiXVxeFnExQ5v4JYUSpg94bWjevMG8JHf1Da5Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -2765,77 +3548,19 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@swc/core": { - "version": "1.10.11", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.11.tgz", - "integrity": "sha512-3zGU5y3S20cAwot9ZcsxVFNsSVaptG+dKdmAxORSE3EX7ixe1Xn5kUwLlgIsM4qrwTUWCJDLNhRS+2HLFivcDg==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.17" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.10.11", - "@swc/core-darwin-x64": "1.10.11", - "@swc/core-linux-arm-gnueabihf": "1.10.11", - "@swc/core-linux-arm64-gnu": "1.10.11", - "@swc/core-linux-arm64-musl": "1.10.11", - "@swc/core-linux-x64-gnu": "1.10.11", - "@swc/core-linux-x64-musl": "1.10.11", - "@swc/core-win32-arm64-msvc": "1.10.11", - "@swc/core-win32-ia32-msvc": "1.10.11", - "@swc/core-win32-x64-msvc": "1.10.11" - }, - "peerDependencies": { - "@swc/helpers": "*" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.10.11", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.11.tgz", - "integrity": "sha512-kKNE2BGu/La2k2WFHovenqZvGQAHRIU+rd2/6a7D6EiQ6EyimtbhUqjCCZ+N1f5fIAnvM+sMdLiQJq4jdd/oOQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, "license": "Apache-2.0" }, - "node_modules/@swc/types": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz", - "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==", - "dev": true, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", "dependencies": { - "@swc/counter": "^0.1.3" + "tslib": "^2.8.0" } }, "node_modules/@testing-library/dom": { @@ -3027,12 +3752,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "license": "MIT" - }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -3391,19 +4110,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@vitejs/plugin-react-swc": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.2.tgz", - "integrity": "sha512-y0byko2b2tSVVf5Gpng1eEhX1OvPC7x8yns1Fx8jDzlJp4LS6CMkCPfLw47cjyoMrshQDoQw4qcgjsU9VvlCew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@swc/core": "^1.7.26" - }, - "peerDependencies": { - "vite": "^4 || ^5 || ^6" - } - }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -3938,7 +4644,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -4016,6 +4722,17 @@ "dev": true, "license": "MIT" }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -4090,7 +4807,6 @@ "version": "1.0.30001695", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz", "integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==", - "dev": true, "funding": [ { "type": "opencollective", @@ -4134,6 +4850,21 @@ "node": ">=10" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -4162,6 +4893,12 @@ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -4195,11 +4932,25 @@ "dev": true, "license": "MIT" }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4212,9 +4963,20 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/colorjs.io": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", @@ -4247,15 +5009,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/core-js-compat": { "version": "3.40.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", @@ -4291,6 +5044,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4522,6 +5285,16 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -4816,55 +5589,14 @@ "license": "MIT", "dependencies": { "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { - "node": ">=18" + "node": ">= 0.4" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/escalade": { @@ -5385,7 +6117,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -5958,7 +6690,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", - "dev": true, "license": "MIT" }, "node_modules/import-fresh": { @@ -6214,7 +6945,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6279,7 +7010,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -6305,7 +7036,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -6857,6 +7588,17 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -7566,7 +8308,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -7639,7 +8381,6 @@ "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "dev": true, "funding": [ { "type": "github", @@ -7661,6 +8402,141 @@ "dev": true, "license": "MIT" }, + "node_modules/next": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/next/-/next-15.1.7.tgz", + "integrity": "sha512-GNeINPGS9c6OZKCvKypbL8GTsT5GhWPp4DM0fzkXJuXMilOO2EeFxuAY6JZbtk6XIl6Ws10ag3xRINDjSO5+wg==", + "license": "MIT", + "dependencies": { + "@next/env": "15.1.7", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.15", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.1.7", + "@next/swc-darwin-x64": "15.1.7", + "@next/swc-linux-arm64-gnu": "15.1.7", + "@next/swc-linux-arm64-musl": "15.1.7", + "@next/swc-linux-x64-gnu": "15.1.7", + "@next/swc-linux-x64-musl": "15.1.7", + "@next/swc-win32-arm64-msvc": "15.1.7", + "@next/swc-win32-x64-msvc": "15.1.7", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -8000,14 +8876,13 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -8105,35 +8980,6 @@ "node": ">= 0.4" } }, - "node_modules/postcss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", - "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8208,6 +9054,13 @@ "dev": true, "license": "MIT" }, + "node_modules/promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==", + "dev": true, + "license": "MIT" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -8361,44 +9214,17 @@ } } }, - "node_modules/react-router": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.5.tgz", - "integrity": "sha512-8BUF+hZEU4/z/JD201yK6S+UYhsf58bzYIDq2NS1iGpwxSXDu7F+DeGSkIXMFBuHZB21FSiCzEcUb18cQNdRkA==", - "license": "MIT", - "dependencies": { - "@types/cookie": "^0.6.0", - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0", - "turbo-stream": "2.4.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-router-dom": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.1.5.tgz", - "integrity": "sha512-/4f9+up0Qv92D3bB8iN5P1s3oHAepSGa9h5k6tpTFlixTTskJZwKGhJ6vRJ277tLD1zuaZTt95hyGWV1Z37csQ==", + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "license": "MIT", - "dependencies": { - "react-router": "7.1.5" - }, "engines": { - "node": ">=20.0.0" + "node": ">= 14.18.0" }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/redent": { @@ -8649,45 +9475,6 @@ "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.1.tgz", - "integrity": "sha512-z+aeEsOeEa3mEbS1Tjl6sAZ8NE3+AalQz1RJGj81M+fizusbdDMoEJwdJNHfaB40Scr4qNu+welOfes7maKonA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.6" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.32.1", - "@rollup/rollup-android-arm64": "4.32.1", - "@rollup/rollup-darwin-arm64": "4.32.1", - "@rollup/rollup-darwin-x64": "4.32.1", - "@rollup/rollup-freebsd-arm64": "4.32.1", - "@rollup/rollup-freebsd-x64": "4.32.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.32.1", - "@rollup/rollup-linux-arm-musleabihf": "4.32.1", - "@rollup/rollup-linux-arm64-gnu": "4.32.1", - "@rollup/rollup-linux-arm64-musl": "4.32.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.32.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.32.1", - "@rollup/rollup-linux-riscv64-gnu": "4.32.1", - "@rollup/rollup-linux-s390x-gnu": "4.32.1", - "@rollup/rollup-linux-x64-gnu": "4.32.1", - "@rollup/rollup-linux-x64-musl": "4.32.1", - "@rollup/rollup-win32-arm64-msvc": "4.32.1", - "@rollup/rollup-win32-ia32-msvc": "4.32.1", - "@rollup/rollup-win32-x64-msvc": "4.32.1", - "fsevents": "~2.3.2" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8783,6 +9570,26 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/sass": { + "version": "1.85.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.1.tgz", + "integrity": "sha512-Uk8WpxM5v+0cMR0XjX9KfRIacmSG86RH4DCCZjLU2rFh5tyutt9siAXJ7G+YfxQ99Q6wrRMbMlVl6KqUms71ag==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, "node_modules/sass-embedded": { "version": "1.83.4", "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.83.4.tgz", @@ -8886,7 +9693,7 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -8895,12 +9702,6 @@ "node": ">=10" } }, - "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "license": "MIT" - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -8950,6 +9751,46 @@ "node": ">= 0.4" } }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9056,6 +9897,23 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -9087,7 +9945,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -9134,6 +9991,14 @@ "node": ">=8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -9320,6 +10185,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9418,7 +10306,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -9519,15 +10407,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, - "node_modules/turbo-stream": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", - "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", - "license": "ISC" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -9849,78 +10730,6 @@ "dev": true, "license": "MIT" }, - "node_modules/vite": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz", - "integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.24.2", - "postcss": "^8.4.49", - "rollup": "^4.23.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/package.json b/package.json index a10ac57..d66a213 100644 --- a/package.json +++ b/package.json @@ -4,23 +4,25 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview", + "dev": "next dev", + "build": "next build", + "start": "next start", "lint": "eslint . --fix", "format:fix": "prettier --write .", "prepare": "husky", "test:watch": "jest --watchAll", - "test": "jest --coverage" + "test": "jest --coverage", + "test:coverage:html": "jest --coverage --coverageReporters=html" }, "dependencies": { "@reduxjs/toolkit": "^2.5.1", "axios": "^1.7.9", "classnames": "^2.5.1", + "next": "^15.1.7", "react": "^18.3.1", "react-dom": "^18.3.1", "react-redux": "^9.2.0", - "react-router-dom": "^7.1.5" + "sass": "^1.85.1" }, "devDependencies": { "@babel/preset-env": "^7.26.8", @@ -34,7 +36,6 @@ "@types/node": "^22.13.1", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", - "@vitejs/plugin-react-swc": "^3.5.0", "eslint": "^9.17.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.2.3", @@ -47,12 +48,12 @@ "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "jest-fetch-mock": "^3.0.3", "prettier": "3.4.2", "sass-embedded": "^1.83.4", "ts-jest": "^29.2.5", "typescript": "~5.6.2", "typescript-eslint": "^8.18.2", - "util": "^0.12.5", - "vite": "^6.0.5" + "util": "^0.12.5" } } diff --git a/src/assets/icons/404.png b/public/icons/404.png similarity index 100% rename from src/assets/icons/404.png rename to public/icons/404.png diff --git a/src/assets/icons/checkbox_false.png b/public/icons/checkbox_false.png similarity index 100% rename from src/assets/icons/checkbox_false.png rename to public/icons/checkbox_false.png diff --git a/src/assets/icons/checkbox_minus.png b/public/icons/checkbox_minus.png similarity index 100% rename from src/assets/icons/checkbox_minus.png rename to public/icons/checkbox_minus.png diff --git a/src/assets/icons/checkbox_true.png b/public/icons/checkbox_true.png similarity index 100% rename from src/assets/icons/checkbox_true.png rename to public/icons/checkbox_true.png diff --git a/src/assets/icons/close.png b/public/icons/close.png similarity index 100% rename from src/assets/icons/close.png rename to public/icons/close.png diff --git a/src/assets/icons/downloads.png b/public/icons/downloads.png similarity index 100% rename from src/assets/icons/downloads.png rename to public/icons/downloads.png diff --git a/src/assets/icons/load.gif b/public/icons/load.gif similarity index 100% rename from src/assets/icons/load.gif rename to public/icons/load.gif diff --git a/src/assets/icons/logo.png b/public/icons/logo.png similarity index 100% rename from src/assets/icons/logo.png rename to public/icons/logo.png diff --git a/src/assets/icons/moon.png b/public/icons/moon.png similarity index 100% rename from src/assets/icons/moon.png rename to public/icons/moon.png diff --git a/src/assets/icons/next.png b/public/icons/next.png similarity index 100% rename from src/assets/icons/next.png rename to public/icons/next.png diff --git a/src/assets/icons/previous.png b/public/icons/previous.png similarity index 100% rename from src/assets/icons/previous.png rename to public/icons/previous.png diff --git a/src/assets/icons/search.png b/public/icons/search.png similarity index 100% rename from src/assets/icons/search.png rename to public/icons/search.png diff --git a/src/assets/icons/sun.png b/public/icons/sun.png similarity index 100% rename from src/assets/icons/sun.png rename to public/icons/sun.png diff --git a/public/nextjs.png b/public/nextjs.png new file mode 100644 index 0000000..e87399b Binary files /dev/null and b/public/nextjs.png differ diff --git a/public/reactjs.png b/public/reactjs.png new file mode 100644 index 0000000..6eb3040 Binary files /dev/null and b/public/reactjs.png differ diff --git a/public/rs-school.webp b/public/rs-school.webp new file mode 100644 index 0000000..9e9514e Binary files /dev/null and b/public/rs-school.webp differ diff --git a/setupTests.ts b/setupTests.ts index 7711bd2..38b9d93 100644 --- a/setupTests.ts +++ b/setupTests.ts @@ -1,4 +1,6 @@ import '@testing-library/jest-dom'; import { TextEncoder } from 'util'; +import fetchMock from 'jest-fetch-mock'; global.TextEncoder = TextEncoder; +fetchMock.enableMocks(); diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index 2a11447..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { - BrowserRouter as Router, - Route, - Routes, - Navigate, -} from 'react-router-dom'; -import ErrorBoundary from './components/shared/error-boundary'; -import './index.scss'; -import HomePage from './pages/home-page'; -import Details from './pages/details-page'; -import NotFound from './pages/not-found-page'; -import TestPage from './pages/test-page'; -import { ThemeProvider } from './context/themeProvider'; - -const App: React.FC = () => { - return ( - - - -
- - } /> - }> - } /> {} - - } /> - } /> - -
-
-
-
- ); -}; - -export default App; diff --git a/src/components/home-page/card-list/index.test.tsx b/src/components/home-page/card-list/index.test.tsx index 375ae2c..e57079f 100644 --- a/src/components/home-page/card-list/index.test.tsx +++ b/src/components/home-page/card-list/index.test.tsx @@ -1,24 +1,10 @@ import { render, screen } from '@testing-library/react'; import { Provider } from 'react-redux'; import { createStore } from 'redux'; -import CardList from './index'; +import CardList from '.'; import { ICharacterDetail } from '../../../models/ICharacterDetail'; import { CharacterDetailsBuilder } from '../../tests/utils/characterDetailsBuilder'; -jest.mock('../../../assets/icons/load.gif', () => 'mocked-load.gif'); -jest.mock( - '../../../assets/icons/checkbox_false.png', - () => 'mocked-checkbox_false.png' -); -jest.mock( - '../../../assets/icons/checkbox_true.png', - () => 'mocked-checkbox_true.png' -); -jest.mock( - '../../../assets/icons/checkbox_minus.png', - () => 'mocked-checkbox_minus.png' -); - jest.mock('./card', () => jest.fn(({ isChecked, onCheckboxChange }) => (
@@ -139,33 +125,6 @@ describe('CardList Component', () => { ).toBeInTheDocument(); }); - /* - test('toggles individual checkboxes', () => { - render( - - - - ); - - const buttons = screen.getAllByText(/Unchecked|Checked/); - expect(buttons[0]).toHaveTextContent('Checked'); - - fireEvent.click(buttons[0]); - expect(buttons[0]).toHaveTextContent('Unchecked'); - - fireEvent.click(buttons[0]); - expect(buttons[0]).toHaveTextContent('Checked'); - }); - */ - test('handles item selection', () => { render( diff --git a/src/components/home-page/card-list/index.tsx b/src/components/home-page/card-list/index.tsx index b1376ba..55a4094 100644 --- a/src/components/home-page/card-list/index.tsx +++ b/src/components/home-page/card-list/index.tsx @@ -1,8 +1,8 @@ +import Image from 'next/image'; import React from 'react'; import { useDispatch } from 'react-redux'; import Card from './card'; import styles from './index.module.scss'; -import loadGif from '../../../assets/icons/load.gif'; import { ICharacterDetail } from '../../../models/ICharacterDetail'; import { togglePeopleSelection } from '../../../store/reducers/SelectedPeoplesSlice'; @@ -30,7 +30,14 @@ const CardList: React.FC = ({ if (loading) { return (
- Loading + Loading
); } diff --git a/src/pages/details-page/index.module.scss b/src/components/home-page/details/index.module.scss similarity index 97% rename from src/pages/details-page/index.module.scss rename to src/components/home-page/details/index.module.scss index 0d6bf13..9de81b1 100644 --- a/src/pages/details-page/index.module.scss +++ b/src/components/home-page/details/index.module.scss @@ -1,4 +1,4 @@ -@use '../../styles/colors.scss' as *; +@use '../../../styles/colors.scss' as *; .detailsSection { position: relative; diff --git a/src/components/home-page/details/index.test.tsx b/src/components/home-page/details/index.test.tsx new file mode 100644 index 0000000..232f35c --- /dev/null +++ b/src/components/home-page/details/index.test.tsx @@ -0,0 +1,125 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import Details from './index'; +import { useSearchParams, usePathname, useRouter } from 'next/navigation'; +import { useFetchByIdQuery } from '../../../services/PeopleService'; +import { Theme, ThemeContext } from '../../../context/themeContext'; +import { CharacterDetailsBuilder } from '../../tests/utils/characterDetailsBuilder'; +import { Provider } from 'react-redux'; +import { createStore } from '@reduxjs/toolkit'; + +jest.mock('next/navigation', () => ({ + useSearchParams: jest.fn(), + usePathname: jest.fn(), + useRouter: jest.fn(), +})); + +jest.mock('../../../services/PeopleService', () => ({ + useFetchByIdQuery: jest.fn(), +})); + +const mockUseSearchParams = useSearchParams as jest.Mock; +const mockUsePathname = usePathname as jest.Mock; +const mockUseRouter = useRouter as jest.Mock; +const mockUseFetchByIdQuery = useFetchByIdQuery as jest.Mock; + +const mockCharacterName = 'Luke Skywalker'; +const mockCharacter = new CharacterDetailsBuilder() + .setName(mockCharacterName) + .setHeight('172') + .setMass('77') + .setGender('male') + .setBirthYear('19BBY') + .setHairColor('blond') + .setSkinColor('fair') + .setEyeColor('blue') + .build(); + +const mockReducer = (state = {}) => state; +const store = createStore(mockReducer); + +describe('Details Component', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUseSearchParams.mockReturnValue(new URLSearchParams({ id: '1' })); + mockUsePathname.mockReturnValue('/details'); + mockUseRouter.mockReturnValue({ push: jest.fn() }); + }); + + const setup = ( + options: { + theme?: Theme; + mockData?: any; + isLoading?: boolean; + error?: Error | null; + id?: string | null; + } = {} + ) => { + const { + theme = 'light', + mockData = null, + isLoading = false, + error = null, + } = options; + + mockUseFetchByIdQuery.mockReturnValue({ + data: mockData, + isLoading, + error, + }); + + return render( + + +
+ + + ); + }; + + test('renders loading state initially', () => { + setup({ isLoading: true }); + + expect(screen.getByAltText('Loading')).toBeInTheDocument(); + }); + + test('fetches and displays character details', async () => { + setup({ mockData: mockCharacter }); + + await waitFor(() => { + expect(screen.getByText(mockCharacterName)).toBeInTheDocument(); + expect(screen.getByText('Height:')).toBeInTheDocument(); + expect(screen.getByText('172')).toBeInTheDocument(); + expect(screen.getByText('Mass:')).toBeInTheDocument(); + expect(screen.getByText('77')).toBeInTheDocument(); + expect(screen.getByText('Gender:')).toBeInTheDocument(); + expect(screen.getByText('male')).toBeInTheDocument(); + }); + }); + + test('displays an error message if fetch fails', async () => { + setup({ error: new Error('Network error') }); + + await waitFor(() => { + expect( + screen.getByText('Failed to load character details.') + ).toBeInTheDocument(); + }); + }); + + test('closes details when the close button is clicked', async () => { + const mockPush = jest.fn(); + mockUseRouter.mockReturnValue({ push: mockPush }); + + setup({ mockData: mockCharacter }); + + await waitFor(() => + expect(screen.getByText(mockCharacterName)).toBeInTheDocument() + ); + + const closeButton = screen.getByText('Close'); + fireEvent.click(closeButton); + + expect(mockPush).toHaveBeenCalledTimes(1); + expect(mockPush).toHaveBeenCalledWith('/details?'); + }); +}); diff --git a/src/pages/details-page/index.tsx b/src/components/home-page/details/index.tsx similarity index 66% rename from src/pages/details-page/index.tsx rename to src/components/home-page/details/index.tsx index d5adbb3..beee44c 100644 --- a/src/pages/details-page/index.tsx +++ b/src/components/home-page/details/index.tsx @@ -1,20 +1,29 @@ +'use client'; + +import { useSearchParams, usePathname, useRouter } from 'next/navigation'; import styles from './index.module.scss'; -import React, { useContext } from 'react'; -import { useParams, useOutletContext } from 'react-router-dom'; -import { useFetchByIdQuery } from '../../services/PeopleService'; -import loadGif from '../../assets/icons/load.gif'; -import closeIcon from '../../assets/icons/close.png'; -import { ThemeContext } from '../../context/themeContext'; +import Image from 'next/image'; +import { useContext } from 'react'; import classNames from 'classnames'; +import { ThemeContext } from '../../../context/themeContext'; +import { useFetchByIdQuery } from '../../../services/PeopleService'; -const Details: React.FC = () => { - const { id } = useParams<{ id: string }>(); - const { handleCloseDetails } = useOutletContext<{ - handleCloseDetails: () => void; - }>(); - const { data: details, isLoading, error } = useFetchByIdQuery(id || ''); +const Details = () => { + const searchParams = useSearchParams(); + const router = useRouter(); + const pathname = usePathname(); const { theme } = useContext(ThemeContext); const isLight = theme === 'light'; + const id = searchParams.get('id'); + const { data: details, isLoading, error } = useFetchByIdQuery(id ?? ''); + + if (!id) return null; + + const handleClose = () => { + const params = new URLSearchParams(searchParams); + params.delete('id'); + router.push(`${pathname}?${params.toString()}`); + }; return (
{ [styles.detailsSection_light]: isLight, })} > - {isLoading ? (
- Loading + Loading
) : error ? (
Failed to load character details.
diff --git a/src/components/home-page/flyout/index.test.tsx b/src/components/home-page/flyout/index.test.tsx index 1cb652d..b7329e0 100644 --- a/src/components/home-page/flyout/index.test.tsx +++ b/src/components/home-page/flyout/index.test.tsx @@ -12,11 +12,6 @@ jest.mock('react-redux', () => ({ useSelector: jest.fn(), })); -jest.mock( - '../../../assets/icons/downloads.png', - () => 'mocked-downloadsIcon.png' -); - describe('Flyout Component', () => { const store = setupStore(); const mockDispatch = jest.fn(); diff --git a/src/components/home-page/flyout/index.tsx b/src/components/home-page/flyout/index.tsx index bada30a..a4f2ec9 100644 --- a/src/components/home-page/flyout/index.tsx +++ b/src/components/home-page/flyout/index.tsx @@ -1,8 +1,8 @@ +import Image from 'next/image'; import React, { useContext } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styles from './index.module.scss'; import classNames from 'classnames'; -import downloadsIcon from '../../../assets/icons/downloads.png'; import { RootState } from '../../../store/store'; import { clearSelections } from '../../../store/reducers/SelectedPeoplesSlice'; import { ThemeContext } from '../../../context/themeContext'; @@ -69,9 +69,12 @@ const Flyout: React.FC = ({ selectedCount }) => { Unselect all @@ -57,7 +60,15 @@ const Pagination: React.FC = ({ data-testid="next-page" onClick={(e) => handlePageChange(e, currentPage + 1)} > - Next next + Next + next
diff --git a/src/components/home-page/search/index.test.tsx b/src/components/home-page/search/index.test.tsx index 8aaa812..45214d7 100644 --- a/src/components/home-page/search/index.test.tsx +++ b/src/components/home-page/search/index.test.tsx @@ -9,7 +9,6 @@ jest.spyOn(Storage.prototype, 'setItem').mockImplementation((key, value) => { console.log(`LocalStorage set: ${key} = ${value}`); }); -jest.mock('../../../assets/icons/search.png', () => 'mocked-search.png'); describe('Search Component', () => { const onSearchMock = jest.fn(); diff --git a/src/components/home-page/search/index.tsx b/src/components/home-page/search/index.tsx index 83b786c..40ab79c 100644 --- a/src/components/home-page/search/index.tsx +++ b/src/components/home-page/search/index.tsx @@ -1,6 +1,7 @@ +import Image from 'next/image'; import React, { useContext } from 'react'; import styles from './index.module.scss'; -import searchIcon from '../../../assets/icons/search.png'; + import useLocalStorage from '../../../hooks/UseLocalStorage'; import { ThemeContext } from '../../../context/themeContext'; import classNames from 'classnames'; @@ -42,7 +43,14 @@ const Search: React.FC = ({ onSearch }) => { placeholder="Search for Star Wars characters..." /> diff --git a/src/components/layout/layout.tsx b/src/components/layout/layout.tsx new file mode 100644 index 0000000..2855f7b --- /dev/null +++ b/src/components/layout/layout.tsx @@ -0,0 +1,27 @@ +import Head from 'next/head'; +import React, { ReactNode } from 'react'; + +type LayoutProps = { + children: ReactNode; + title?: string; + description?: string; +}; + +export default function Layout({ + children, + title = 'My App', + description = 'Test Next.js app with TypeScript', +}: LayoutProps) { + return ( + <> + + {title} + + + + + +
{children}
+ + ); +} diff --git a/src/components/shared/themeToggle/index.test.tsx b/src/components/shared/themeToggle/index.test.tsx index ee4c6f7..9bfc29b 100644 --- a/src/components/shared/themeToggle/index.test.tsx +++ b/src/components/shared/themeToggle/index.test.tsx @@ -2,9 +2,6 @@ import { render, fireEvent } from '@testing-library/react'; import { ThemeContext, ThemeContextType } from '../../../context/themeContext'; import ThemeToggle from '.'; -jest.mock('../../../assets/icons/moon.png', () => 'mocked-moon.png'); -jest.mock('../../../assets/icons/sun.png', () => 'mocked-sun.png'); - const mockThemeContext: ThemeContextType = { theme: 'light', toggleTheme: jest.fn(), diff --git a/src/components/shared/themeToggle/index.tsx b/src/components/shared/themeToggle/index.tsx index b622db3..2d8d2f2 100644 --- a/src/components/shared/themeToggle/index.tsx +++ b/src/components/shared/themeToggle/index.tsx @@ -1,8 +1,7 @@ +import Image from 'next/image'; import React, { useContext } from 'react'; import { ThemeContext } from '../../../context/themeContext'; import styles from './index.module.scss'; -import moonIcon from '../../../assets/icons/moon.png'; -import sunIcon from '../../../assets/icons/sun.png'; import classNames from 'classnames'; const ThemeToggle: React.FC = () => { @@ -16,9 +15,12 @@ const ThemeToggle: React.FC = () => { [styles.toggle_light]: isLight, })} > - Theme Icon {theme === 'dark' ? 'Dark' : 'Light'} diff --git a/src/context/themeProvider.tsx b/src/context/themeProvider.tsx index 47e4392..081bd58 100644 --- a/src/context/themeProvider.tsx +++ b/src/context/themeProvider.tsx @@ -1,3 +1,5 @@ +'use client'; + import { useState, useEffect, ReactNode, FC } from 'react'; import { ThemeContext } from './themeContext'; @@ -8,9 +10,13 @@ interface ThemeProviderProps { } export const ThemeProvider: FC = ({ children }) => { - const [theme, setTheme] = useState(() => { - return (localStorage.getItem('theme') as Theme) || 'dark'; - }); + const [theme, setTheme] = useState('dark'); + useEffect(() => { + const storedTheme = localStorage.getItem('theme') as Theme | null; + if (storedTheme) { + setTheme(storedTheme); + } + }, []); useEffect(() => { document.body.className = theme; diff --git a/src/index.css b/src/index.css deleted file mode 100644 index d27ad4e..0000000 --- a/src/index.css +++ /dev/null @@ -1,4 +0,0 @@ -.app { - padding: 20px; - font-family: Arial, sans-serif; -} diff --git a/src/main.tsx b/src/main.tsx deleted file mode 100644 index 79c8bcd..0000000 --- a/src/main.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import './index.css'; -import App from './App.tsx'; -import { Provider } from 'react-redux'; -import { setupStore } from './store/store.ts'; - -const rootElement = document.getElementById('root'); -const store = setupStore(); - -if (!rootElement) { - throw new Error('Root element not found'); -} -createRoot(rootElement).render( - - - - - -); diff --git a/src/pages/404.tsx b/src/pages/404.tsx new file mode 100644 index 0000000..085e5ef --- /dev/null +++ b/src/pages/404.tsx @@ -0,0 +1,5 @@ +import NotFound from './not-found-page'; + +export default function NotFoundPage() { + return ; +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx new file mode 100644 index 0000000..ee5c34d --- /dev/null +++ b/src/pages/_app.tsx @@ -0,0 +1,19 @@ +import type { AppProps } from 'next/app'; +import '../styles/globals.scss'; +import { Providers } from './providers'; +import { ThemeProvider } from '../context/themeProvider'; +import Layout from '../components/layout/layout'; + +function App({ Component, pageProps }: AppProps) { + return ( + + + + + + + + ); +} + +export default App; diff --git a/src/pages/details-page/index.test.tsx b/src/pages/details-page/index.test.tsx deleted file mode 100644 index 55e3e47..0000000 --- a/src/pages/details-page/index.test.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { - MemoryRouter, - Route, - Routes, - useOutletContext, - useParams, -} from 'react-router-dom'; -import Details from './index'; -import { CharacterDetailsBuilder } from '../../components/tests/utils/characterDetailsBuilder'; -import { useFetchByIdQuery } from '../../services/PeopleService'; - -jest.mock('../../assets/icons/load.gif', () => 'mocked-load.gif'); -jest.mock('../../assets/icons/close.png', () => 'mocked-close.png'); - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: jest.fn(), - useOutletContext: jest.fn(), -})); - -jest.mock('../../services/PeopleService', () => ({ - useFetchByIdQuery: jest.fn(), -})); - -const mockUseParams = useParams as jest.Mock; -const mockUseOutletContext = useOutletContext as jest.Mock; -const mockUseFetchByIdQuery = useFetchByIdQuery as jest.Mock; - -const mockCharacterName = 'Luke Skywalker'; -const mockCharacter = new CharacterDetailsBuilder() - .setName(mockCharacterName) - .setHeight('172') - .setMass('77') - .setGender('male') - .setBirthYear('19BBY') - .setHairColor('blond') - .setSkinColor('fair') - .setEyeColor('blue') - .build(); - -describe('Details Component', () => { - beforeEach(() => { - jest.clearAllMocks(); - mockUseParams.mockReturnValue({ id: '1' }); - mockUseOutletContext.mockReturnValue({ handleCloseDetails: jest.fn() }); - }); - - test('renders loading state initially', () => { - mockUseFetchByIdQuery.mockReturnValue({ - data: null, - isLoading: true, - error: null, - }); - - render( - - - } /> - - - ); - - expect(screen.getByAltText('Loading')).toBeInTheDocument(); - }); - - test('fetches and displays character details', async () => { - mockUseFetchByIdQuery.mockReturnValue({ - data: mockCharacter, - isLoading: false, - error: null, - }); - - render( - - - } /> - - - ); - - await waitFor(() => { - expect(screen.getByText(mockCharacterName)).toBeInTheDocument(); - expect(screen.getByText('Height:')).toBeInTheDocument(); - expect(screen.getByText('172')).toBeInTheDocument(); - expect(screen.getByText('Mass:')).toBeInTheDocument(); - expect(screen.getByText('77')).toBeInTheDocument(); - expect(screen.getByText('Gender:')).toBeInTheDocument(); - expect(screen.getByText('male')).toBeInTheDocument(); - }); - }); - - test('displays an error message if fetch fails', async () => { - mockUseFetchByIdQuery.mockReturnValue({ - data: null, - isLoading: false, - error: new Error('Network error'), - }); - - render( - - - } /> - - - ); - - await waitFor(() => { - expect( - screen.getByText('Failed to load character details.') - ).toBeInTheDocument(); - }); - }); - - test('closes details when the close button is clicked', async () => { - const mockHandleCloseDetails = jest.fn(); - mockUseFetchByIdQuery.mockReturnValue({ - data: mockCharacter, - isLoading: false, - error: null, - }); - mockUseOutletContext.mockReturnValue({ - handleCloseDetails: mockHandleCloseDetails, - }); - - render( - - - } /> - - - ); - - await waitFor(() => - expect(screen.getByText(mockCharacterName)).toBeInTheDocument() - ); - - const closeButton = screen.getByText('Close'); - fireEvent.click(closeButton); - - expect(mockHandleCloseDetails).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/pages/home-page/index.module.scss b/src/pages/home-page/index.module.scss index 408e624..4273fc7 100644 --- a/src/pages/home-page/index.module.scss +++ b/src/pages/home-page/index.module.scss @@ -27,8 +27,9 @@ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); .logo { - img { + .logoIcon { height: 30px; + width: 30px; transition: color 0.3s ease, transform 0.3s ease; @@ -82,18 +83,10 @@ width: 100%; margin-top: 10px; } - - h1 { - font-size: 24px; - } } @media (max-width: 480px) { .topMenu { padding: 10px; } - - h1 { - font-size: 20px; - } } diff --git a/src/pages/home-page/index.test.tsx b/src/pages/home-page/index.test.tsx index a732cc1..5510ff9 100644 --- a/src/pages/home-page/index.test.tsx +++ b/src/pages/home-page/index.test.tsx @@ -1,241 +1,154 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { MemoryRouter, Route, Routes } from 'react-router-dom'; -import HomePage from './index'; - -import { act } from 'react'; -import { useFetchAllQuery } from '../../services/PeopleService'; -import { setupStore } from '../../store/store'; -import { ThemeContext, ThemeContextType } from '../../context/themeContext'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { useRouter } from 'next/navigation'; +import Details from '../../components/home-page/details'; +import { CharacterDetailsBuilder } from '../../components/tests/utils/characterDetailsBuilder'; import { Provider } from 'react-redux'; +import { createStore } from '@reduxjs/toolkit'; +import { useFetchByIdQuery } from '../../services/PeopleService'; +jest.mock('next/navigation', () => ({ + useSearchParams: jest.fn(() => new URLSearchParams('?id=123')), + useRouter: jest.fn(() => ({ push: jest.fn() })), + usePathname: jest.fn(() => '/home'), +})); jest.mock('../../services/PeopleService', () => ({ - peopleAPI: { - reducerPath: 'peopleAPI', - reducer: () => ({}), - middleware: () => (next: (action: unknown) => void) => (action: unknown) => - next(action), - }, - useFetchAllQuery: jest.fn(), + useFetchByIdQuery: jest.fn(), })); +const mockUseFetchByIdQuery = useFetchByIdQuery as jest.Mock; global.fetch = jest.fn(); - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useNavigate: jest.fn(), -})); - -jest.mock('../../assets/icons/logo.png', () => 'mocked-logoIcon.png'); -jest.mock('../../assets/icons/load.gif', () => 'mocked-load.gif'); -jest.mock('../../assets/icons/search.png', () => 'mocked-search.png'); -jest.mock('../../assets/icons/next.png', () => 'mocked-next.png'); -jest.mock('../../assets/icons/previous.png', () => 'mocked-previous.png'); -jest.mock('../../assets/icons/downloads.png', () => 'mocked-downloads.png'); -jest.mock('../../assets/icons/moon.png', () => 'mocked-moon.png'); -jest.mock('../../assets/icons/sun.png', () => 'mocked-sun.png'); - -jest.mock( - '../../assets/icons/checkbox_false.png', - () => 'mocked-checkbox_false.png' -); -jest.mock( - '../../assets/icons/checkbox_true.png', - () => 'mocked-checkbox_true.png' -); -jest.mock( - '../../assets/icons/checkbox_minus.png', - () => 'mocked-checkbox_minus.png' -); - -describe('HomePage Component', () => { - const store = setupStore(); - - const setup = ({ mockThemeContext = defaultThemeContext } = {}) => - render( - - - - - - - - ); +const mockReducer = (state = {}) => state; +const store = createStore(mockReducer); + +const mockUseRouter = useRouter as jest.Mock; +const mockCharacterName = 'Luke Skywalker'; +const mockCharacter = new CharacterDetailsBuilder() + .setName(mockCharacterName) + .setHeight('172') + .setMass('77') + .setGender('male') + .setBirthYear('19BBY') + .setHairColor('blond') + .setSkinColor('fair') + .setEyeColor('blue') + .build(); + +describe('Details Component', () => { + let consoleErrorMock: jest.SpyInstance; beforeEach(() => { jest.clearAllMocks(); - (useFetchAllQuery as jest.Mock).mockReturnValue({ - data: { results: [], count: 0 }, - isLoading: false, - error: null, - }); + consoleErrorMock = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + mockUseRouter.mockReturnValue({ query: { id: '1' }, push: jest.fn() }); }); - const defaultThemeContext: ThemeContextType = { - theme: 'light', - toggleTheme: jest.fn(), - }; - - const mockSkywalker = { - name: 'Luke Skywalker', - url: 'https://swapi.dev/api/people/1/', - }; - - const mockDarthVader = { - name: 'Darth Vader', - url: 'https://swapi.dev/api/people/4/', - }; - - test('renders logo and search bar', async () => { - await act(async () => { - setup(); - }); - expect(screen.getByAltText('logo')).toBeInTheDocument(); - expect(screen.getByText('Search')).toBeInTheDocument(); + afterEach(() => { + consoleErrorMock.mockRestore(); }); - it('handles search input correctly', async () => { - setup(); - - const searchInput = screen.getByRole('textbox'); - fireEvent.change(searchInput, { target: { value: 'Luke' } }); - - await waitFor(() => { - expect(localStorage.getItem('searchItem')).toBe('Luke'); + test('renders loading state initially', () => { + (fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => mockCharacter, }); - }); - it('updates pagination correctly', async () => { - (useFetchAllQuery as jest.Mock).mockReturnValue({ - data: { - results: [mockSkywalker], - count: 40, - }, + mockUseFetchByIdQuery.mockReturnValue({ + data: mockCharacter, isLoading: false, error: null, }); render( - - - - } /> - - - +
); - - await waitFor(() => { - expect(screen.getByText(/Page 2/i)).toBeInTheDocument(); - }); - }); - - it('displays loading state correctly', async () => { - (useFetchAllQuery as jest.Mock).mockReturnValue({ - data: null, - isLoading: true, - error: null, - }); - - setup(); - expect(screen.getByAltText('Loading')).toBeInTheDocument(); }); - it('displays error message when API call fails', async () => { - (useFetchAllQuery as jest.Mock).mockReturnValue({ - data: null, - isLoading: false, - error: true, + test('fetches and displays character details', async () => { + (fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => mockCharacter, }); - setup(); + render( + +
+ + ); - expect( - await screen.findByText( - /Oops! Something went wrong. Please check your internet connection./i - ) - ).toBeInTheDocument(); + expect(await screen.findByText(mockCharacterName)).toBeInTheDocument(); + expect(screen.getByText('Height:')).toBeInTheDocument(); + expect(screen.getByText('172')).toBeInTheDocument(); + expect(screen.getByText('Mass:')).toBeInTheDocument(); + expect(screen.getByText('77')).toBeInTheDocument(); + expect(screen.getByText('Gender:')).toBeInTheDocument(); + expect(screen.getByText('male')).toBeInTheDocument(); }); - test('renders "No results found" when results are empty', async () => { - (useFetchAllQuery as jest.Mock).mockReturnValue({ - data: { results: [], count: 0 }, - isLoading: false, - error: null, - }); + test('displays an error message if fetch fails', async () => { + (fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error')); - setup(); + render( + +
+ + ); expect( - screen.getByText( - /No results found. Please try a different search query./i - ) + await screen.findByText(/Failed to fetch details/i) ).toBeInTheDocument(); + expect(consoleErrorMock).toHaveBeenCalledWith( + expect.stringContaining('Failed to fetch details:'), + expect.any(Error) + ); }); - test('renders character cards when results are available', async () => { - (useFetchAllQuery as jest.Mock).mockReturnValue({ - data: { - results: [mockSkywalker, mockDarthVader], - count: 2, - }, - isLoading: false, - error: null, - }); - - setup(); - - expect(screen.getByText(/Luke Skywalker/i)).toBeInTheDocument(); - expect(screen.getByText(/Darth Vader/i)).toBeInTheDocument(); - }); + test('closes details when the close button is clicked', async () => { + const mockPush = jest.fn(); + mockUseRouter.mockReturnValue({ query: { id: '1' }, push: mockPush }); - test('renders pagination when results are available and not loading', async () => { - (useFetchAllQuery as jest.Mock).mockReturnValue({ - data: { - results: [mockSkywalker], - count: 20, - }, - isLoading: false, - error: null, + (fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => mockCharacter, }); - setup(); - - await waitFor(() => { - expect(screen.getByText(/Next/i)).toBeInTheDocument(); - }); - }); + render( + +
+ + ); - test('does not render pagination when loading', async () => { - (useFetchAllQuery as jest.Mock).mockReturnValue({ - data: { - results: [mockSkywalker], - count: 20, - }, - isLoading: true, - error: null, - }); + expect(await screen.findByText(mockCharacterName)).toBeInTheDocument(); - setup(); + const closeButton = screen.getByText('Close'); + fireEvent.click(closeButton); - expect(screen.queryByText(/Next/i)).not.toBeInTheDocument(); + expect(mockPush).toHaveBeenCalledWith('/home'); }); - test('toggles theme when theme button is clicked', async () => { - const mockToggleTheme = jest.fn(); - const mockThemeContext: ThemeContextType = { - theme: 'dark', - toggleTheme: mockToggleTheme, - }; - - setup({ mockThemeContext }); - - const toggleButton = screen.getByRole('button', { name: /dark/i }); + test('handles HTTP error when response is not ok', async () => { + (fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 404, + json: async () => ({ message: 'Not found' }), + }); - fireEvent.click(toggleButton); + render( + +
+ + ); - expect(mockToggleTheme).toHaveBeenCalledTimes(1); + expect( + await screen.findByText(/Failed to fetch details/i) + ).toBeInTheDocument(); + expect(consoleErrorMock).toHaveBeenCalledWith( + expect.stringContaining('Failed to fetch details:'), + expect.any(Error) + ); }); }); diff --git a/src/pages/home-page/index.tsx b/src/pages/home-page/index.tsx index 901d3e3..66a3a80 100644 --- a/src/pages/home-page/index.tsx +++ b/src/pages/home-page/index.tsx @@ -1,31 +1,32 @@ +import Image from 'next/image'; +import styles from './index.module.scss'; import React, { useState, useEffect, useContext } from 'react'; -import { useNavigate, useLocation, Outlet } from 'react-router-dom'; +import { useSearchParams, usePathname, useRouter } from 'next/navigation'; import { useSelector } from 'react-redux'; - -import Pagination from '../../components/home-page/pagination'; +import classNames from 'classnames'; import { ITEMS_PER_PAGE } from '../../consts/constants'; -import logoIcon from '../../assets/icons/logo.png'; +import { ThemeContext } from '../../context/themeContext'; +import Pagination from '../../components/home-page/pagination'; import Search from '../../components/home-page/search'; import ThemeToggle from '../../components/shared/themeToggle'; -import { ThemeContext } from '../../context/themeContext'; -import classNames from 'classnames'; -import styles from './index.module.scss'; import { useFetchAllQuery } from '../../services/PeopleService'; import { RootState } from '../../store/store'; import Flyout from '../../components/home-page/flyout'; import CardList from '../../components/home-page/card-list'; +import Details from '../../components/home-page/details'; +import Link from 'next/link'; const HomePage: React.FC = () => { const [currentPage, setCurrentPage] = useState(1); - const [searchItem, setSearchItem] = useState( - localStorage.getItem('searchItem') ?? '' - ); - const navigate = useNavigate(); - const location = useLocation(); + const [searchItem, setSearchItem] = useState(''); const { theme } = useContext(ThemeContext); - const isLight = theme === 'light'; + const searchParams = useSearchParams(); + const id = searchParams.get('id'); + const router = useRouter(); + const pathname = usePathname(); + const { data, error, isLoading } = useFetchAllQuery({ search: searchItem, page: currentPage, @@ -39,58 +40,43 @@ const HomePage: React.FC = () => { ); const selectedCount = Object.values(selectedPeoples).filter(Boolean).length; + useEffect(() => { + const storedSearchItem = localStorage.getItem('searchItem'); + if (storedSearchItem) { + setSearchItem(storedSearchItem); + } + }, []); + + useEffect(() => { + if (data) { + const pageParam = searchParams.get('frontpage'); + const page = pageParam ? parseInt(pageParam, 10) : 1; + + if (totalPages && (page < 1 || page > totalPages)) { + router.push('/not-found'); + } else { + setCurrentPage(page); + } + } + }, [data, totalPages, searchParams, router]); + const handleSearch = (term: string) => { setCurrentPage(1); - updateUrl(1, null); + router.push(`${pathname}?frontpage=1`); setSearchItem(term); localStorage.setItem('searchItem', term); }; const handlePageChange = (page: number) => { - updateUrl(page, null); + router.push(`${pathname}?frontpage=${page}`); setCurrentPage(page); }; const handleItemClick = (id: string) => { - updateUrl(currentPage, id); - }; - - const handleCloseDetails = () => { - updateUrl(currentPage, null); + router.push(`${pathname}?frontpage=${currentPage}&id=${id}`); }; - const handleLeftSectionClick = () => { - if (showDetails) { - handleCloseDetails(); - } - }; - - const updateUrl = (page: number, detailsId: string | null) => { - const searchParams = new URLSearchParams(location.search); - searchParams.set('frontpage', page.toString()); - - let newPath; - if (detailsId) { - newPath = `/home/details/${detailsId}`; - } else { - newPath = '/home'; - } - - navigate(`${newPath}?${searchParams.toString()}`); - }; - - const showDetails = location.pathname.includes('details'); - - useEffect(() => { - const searchParams = new URLSearchParams(location.search); - const page = parseInt(searchParams.get('frontpage') ?? '1', 10); - - if (totalPages && (page < 1 || page > totalPages)) { - navigate('/not-found', { replace: true }); - } else { - setCurrentPage(page); - } - }, [totalPages, navigate, location.search]); + const showDetails = Boolean(id); return ( <> @@ -100,7 +86,16 @@ const HomePage: React.FC = () => { })} >
- logo + + logo +
@@ -112,7 +107,7 @@ const HomePage: React.FC = () => {

Star Wars Character Search

-
+
{ /> )}
+ {showDetails && (
- +
)}
diff --git a/src/pages/home.tsx b/src/pages/home.tsx new file mode 100644 index 0000000..7d3a055 --- /dev/null +++ b/src/pages/home.tsx @@ -0,0 +1,7 @@ +'use client'; + +import HomePage from './home-page'; + +export default function Home() { + return ; +} diff --git a/src/pages/index-page/index.module.scss b/src/pages/index-page/index.module.scss new file mode 100644 index 0000000..795e54e --- /dev/null +++ b/src/pages/index-page/index.module.scss @@ -0,0 +1,114 @@ +@use '../../styles/colors.scss' as *; + +@keyframes gradientAnimation { + 0% { + background-position: 0% 25%; + background-color: #cfff5f; + } + 25% { + background-position: 50% 50%; + background-color: #ff5f5f; + } + 50% { + background-position: 100% 50%; + background-color: #e7a10a; + } + 75% { + background-position: 50% 50%; + background-color: #ff7e5f; + } + 100% { + background-position: 0% 25%; + background-color: #a50c0c; + } +} + +.container { + background-color: #00000013; + color: $text-light; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 20px; + + background-image: linear-gradient(to right, #333 1px, transparent 1px), + linear-gradient(to bottom, #333 1px, transparent 1px); + background-size: 40px 40px; + + .title { + font-size: 2rem; + margin-bottom: 10px; + background-image: linear-gradient(90deg, #ff3300, #fbff1e, #ff9900); + background-size: 200% 100%; + background-clip: text; + -webkit-background-clip: text; + color: transparent; + animation: gradientAnimation 3s infinite linear; + display: inline-block; + text-transform: uppercase; + } + + .description { + font-size: 1.2rem; + max-width: 600px; + margin-bottom: 20px; + } + + .imageGrid { + display: flex; + gap: 20px; + justify-content: center; + align-items: center; + flex-wrap: wrap; + margin-bottom: 20px; + + .imageContainer { + display: flex; + flex-direction: column; + align-items: center; + } + + .imageText { + margin-top: 8px; + font-size: 1rem; + color: $text-dark; + } + } + + .link { + font-size: 1.2rem; + + color: #eeff00; + text-decoration: none; + margin-top: 20px; + transition: color 0.3s ease-in-out; + + &:hover { + color: #ff9900; + } + } + + &_light { + background-image: linear-gradient( + to right, + $light-input 1px, + transparent 1px + ), + linear-gradient(to bottom, $light-input 1px, transparent 1px); + background-size: 40px 40px; + + background-color: #00000013; + color: $text-black; + + .link { + color: #ff9900; + + &:hover { + color: #ff5e1e; + } + } + } +} diff --git a/src/pages/index-page/index.tsx b/src/pages/index-page/index.tsx new file mode 100644 index 0000000..9c91a18 --- /dev/null +++ b/src/pages/index-page/index.tsx @@ -0,0 +1,76 @@ +//'use client'; + +import Head from 'next/head'; +import Image from 'next/image'; +import Link from 'next/link'; +import styles from './index.module.scss'; +import { useContext } from 'react'; +import classNames from 'classnames'; +import { ThemeContext } from '../../context/themeContext'; + +const IndexPage: React.FC = () => { + const { theme } = useContext(ThemeContext); + const isLight = theme === 'light'; + + return ( + <> + + Test App + + + + +
+

RS School React Labs Project

+

+ This project is developed as part task in the RS School React course. + It utilizes Next.js for server-side rendering and enhanced + performance. +

+
+
+ RS School Logo +

RS School

+
+
+ React.js Logo +

React.js

+
+
+ Next.js Logo +

Next.js

+
+
+ +
+ + ); +}; + +export default IndexPage; diff --git a/src/pages/index.tsx b/src/pages/index.tsx new file mode 100644 index 0000000..0a2cd13 --- /dev/null +++ b/src/pages/index.tsx @@ -0,0 +1,5 @@ +import IndexPage from './index-page'; + +export default function Home() { + return ; +} diff --git a/src/pages/not-found-page/index.module.scss b/src/pages/not-found-page/index.module.scss index 2a91b29..6788875 100644 --- a/src/pages/not-found-page/index.module.scss +++ b/src/pages/not-found-page/index.module.scss @@ -1,28 +1,144 @@ @use '../../styles/colors.scss' as *; .notFound { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + color: $text-light; + text-align: center; + padding: 20px; + font-family: 'Roboto', sans-serif; + + .header { + font-size: 4rem; + color: $red-button; + margin-bottom: 20px; + animation: fadeIn 1s ease-out; + + &:hover { + color: $red-button-hover; + } + } + + .description, + .redirect { + font-size: 1.2rem; + color: $text-light; + margin: 10px 0; + max-width: 600px; + animation: fadeIn 1s ease-out; + } + .pageNotFoundContainer { display: flex; justify-content: center; align-items: center; + margin: 30px 0; .pageNotFoundImage { - width: 20%; - height: 20%; + width: 250px; + height: auto; + animation: zoomIn 1.5s ease-out; } } .linkGoHome { - color: $text-dark; + font-size: 1.5rem; + color: $text-light; + background-color: $red-button; + padding: 10px 30px; + border-radius: 30px; text-decoration: none; - transition: color 0.3s ease-in-out; + display: inline-block; + transition: all 0.3s ease; + margin-top: 20px; &:hover { - color: $red-button; + background-color: $red-button-hover; + transform: scale(1.05); } &:active { - color: $red-button-hover; + background-color: $red-button-hover; + transform: scale(1); + } + } +} + +@keyframes fadeIn { + 0% { + opacity: 0; + transform: translateY(-20px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes zoomIn { + 0% { + transform: scale(0.8); + opacity: 0; + } + 100% { + transform: scale(1); + opacity: 1; + } +} + +@media (max-width: 768px) { + .notFound { + padding: 15px; + + .header { + font-size: 2.5rem; + margin-bottom: 15px; + } + + .description, + .redirect { + font-size: 1rem; + max-width: 90%; + } + + .pageNotFoundContainer { + .pageNotFoundImage { + width: 150px; + } + } + + .linkGoHome { + font-size: 1.2rem; + padding: 8px 20px; + } + } +} + +@media (max-width: 480px) { + .notFound { + .header { + font-size: 2rem; + } + + .description, + .redirect { + font-size: 0.9rem; + } + + .pageNotFoundContainer { + margin: 20px 0; + + .pageNotFoundImage { + width: 100px; + } + } + + .linkGoHome { + font-size: 1.1rem; + padding: 8px 15px; } } } diff --git a/src/pages/not-found-page/index.test.tsx b/src/pages/not-found-page/index.test.tsx index 8ee3fac..339244e 100644 --- a/src/pages/not-found-page/index.test.tsx +++ b/src/pages/not-found-page/index.test.tsx @@ -1,46 +1,34 @@ import { render, screen } from '@testing-library/react'; -import { BrowserRouter as Router } from 'react-router-dom'; -import NotFound from '.'; +import NotFound from '../404'; +import '@testing-library/jest-dom'; -jest.mock('../../assets/icons/404.png', () => 'mocked-404.png'); +describe('NotFound Component', () => { + it('renders the 404 message', () => { + render(); -describe('NotFound component', () => { - const renderNotFound = () => { - render( - - - - ); - }; + expect(screen.getByText('404 - Page Not Found')).toBeInTheDocument(); - test('renders 404 page with correct text', () => { - renderNotFound(); - - expect(screen.getByText(/404 - Page Not Found/i)).toBeInTheDocument(); expect( - screen.getByText(/The page you are looking for does not exist./i) + screen.getByText('The page you are looking for does not exist.') ).toBeInTheDocument(); + expect( - screen.getByText( - /You will be automatically redirected to the home page./i - ) + screen.getByText('You will be automatically redirected to the home page.') ).toBeInTheDocument(); }); - test('renders image with 404 class', () => { - renderNotFound(); + it('renders the 404 image', () => { + render(); - const image = screen.getByAltText('404') as HTMLImageElement; + const image = screen.getByAltText('404'); expect(image).toBeInTheDocument(); - expect(image).toHaveClass('pageNotFoundImage'); - expect(image.src).toContain('mocked-404.png'); }); - test('renders "Go to Home" link', () => { - renderNotFound(); + it('renders the "Go to Home" link', () => { + render(); - const link = screen.getByText(/Go to Home/i); + const link = screen.getByRole('link', { name: /go to home/i }); expect(link).toBeInTheDocument(); - expect(link).toHaveAttribute('href', '/'); + expect(link).toHaveAttribute('href', '/home'); }); }); diff --git a/src/pages/not-found-page/index.tsx b/src/pages/not-found-page/index.tsx index 6f08228..117199c 100644 --- a/src/pages/not-found-page/index.tsx +++ b/src/pages/not-found-page/index.tsx @@ -1,21 +1,30 @@ -import image404 from '../../assets/icons/404.png'; -import { Link } from 'react-router-dom'; +import Image from 'next/image'; +import Link from 'next/link'; import styles from './index.module.scss'; -const NotFound = () => { +export default function NotFound() { return (
-

404 - Page Not Found

-

The page you are looking for does not exist.

-

You will be automatically redirected to the home page.

+

404 - Page Not Found

+

+ The page you are looking for does not exist. +

+

+ You will be automatically redirected to the home page. +

- 404 + 404
- + Go to Home
); -}; - -export default NotFound; +} diff --git a/src/pages/providers.tsx b/src/pages/providers.tsx new file mode 100644 index 0000000..99b3752 --- /dev/null +++ b/src/pages/providers.tsx @@ -0,0 +1,10 @@ +'use client'; + +import { Provider } from 'react-redux'; +import { setupStore } from '../store/store'; + +const store = setupStore(); + +export function Providers({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/src/pages/test-page/index.test.tsx b/src/pages/test-page/index.test.tsx deleted file mode 100644 index 751edfe..0000000 --- a/src/pages/test-page/index.test.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; -import TestPage from './index'; -import { useParams } from 'react-router-dom'; - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: jest.fn(), -})); - -describe('TestPage', () => { - it('renders correctly with mocked id from URL', () => { - (useParams as jest.Mock).mockReturnValue({ id: '123' }); - - render( - - - - ); - - expect(screen.getByText('TestPaget123')).toBeInTheDocument(); - }); - - it('renders correctly with a different mocked id', () => { - (useParams as jest.Mock).mockReturnValue({ id: '456' }); - - render( - - - - ); - - expect(screen.getByText('TestPaget456')).toBeInTheDocument(); - }); -}); diff --git a/src/pages/test-page/index.tsx b/src/pages/test-page/index.tsx deleted file mode 100644 index 907613c..0000000 --- a/src/pages/test-page/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { useParams } from 'react-router-dom'; - -const TestPage: React.FC = () => { - const { id } = useParams<{ id: string }>(); - return
TestPaget{id}
; -}; - -export default TestPage; diff --git a/src/services/PeopleService.test.tsx b/src/services/PeopleService.test.tsx new file mode 100644 index 0000000..b601a0d --- /dev/null +++ b/src/services/PeopleService.test.tsx @@ -0,0 +1,182 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { useFetchAllQuery, useFetchByIdQuery } from './PeopleService'; +import fetchMock from 'jest-fetch-mock'; +import { setupStore } from '../store/store'; +import { ReactNode } from 'react'; +import { API_BASE_URL } from '../consts/urls'; + +const store = setupStore(); +describe('peopleAPI', () => { + function wrapper({ children }: { children: ReactNode }) { + return {children}; + } + + beforeEach(() => { + fetchMock.enableMocks(); + }); + + afterEach(() => { + fetchMock.resetMocks(); + fetchMock.disableMocks(); + }); + + it('fetches a list of characters', async () => { + const endpointName = 'fetchAll'; + const people = '/people/'; + const data = { + results: [{ id: '1', name: 'Luke Skywalker' }], + count: 1, + }; + const searchItem = 'searchItem'; + const currentPage = 2; + const url = `${API_BASE_URL}${people}?search=${searchItem}&page=${currentPage}`; + fetchMock.mockOnceIf(url, () => + Promise.resolve({ + status: 200, + body: JSON.stringify(data), + }) + ); + const { result } = renderHook( + () => + useFetchAllQuery({ + search: searchItem, + page: currentPage, + }), + { + wrapper, + } + ); + + expect(result.current).toMatchObject({ + status: 'pending', + endpointName, + isLoading: true, + isSuccess: false, + isError: false, + isFetching: true, + }); + + await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1)); + const calledRequest = fetchMock.mock.calls[0][0]; + + const calledUrl = + calledRequest instanceof Request ? calledRequest.url : calledRequest; + expect(calledUrl).toBe(url); + + expect(result.current).toMatchObject({ + status: 'fulfilled', + endpointName, + data, + isLoading: false, + isSuccess: true, + isError: false, + currentData: data, + isFetching: false, + }); + }); + + it('fetches a list of characters without searchItem and currentPage', async () => { + const endpointName = 'fetchAll'; + const people = '/people/'; + const data = { + results: [{ id: '1', name: 'Luke Skywalker' }], + count: 1, + }; + const searchItem = undefined; + const currentPage = undefined; + const url = `${API_BASE_URL}${people}?search=&page=1`; + fetchMock.mockOnceIf(url, () => + Promise.resolve({ + status: 200, + body: JSON.stringify(data), + }) + ); + const { result } = renderHook( + () => + useFetchAllQuery({ + search: searchItem, + page: currentPage, + }), + { + wrapper, + } + ); + + expect(result.current).toMatchObject({ + status: 'pending', + endpointName, + isLoading: true, + isSuccess: false, + isError: false, + isFetching: true, + }); + + await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1)); + const calledRequest = fetchMock.mock.calls[0][0]; + + const calledUrl = + calledRequest instanceof Request ? calledRequest.url : calledRequest; + expect(calledUrl).toBe(url); + + expect(result.current).toMatchObject({ + status: 'fulfilled', + endpointName, + data, + isLoading: false, + isSuccess: true, + isError: false, + currentData: data, + isFetching: false, + }); + }); + + it('fetches a character by ID', async () => { + const endpointName = 'fetchById'; + const characterId = '1'; + const urlFetchById = `${API_BASE_URL}/people/${characterId}/`; + const characterData = { + id: '1', + name: 'Luke Skywalker', + height: '172', + mass: '77', + }; + fetchMock.mockOnceIf(urlFetchById, JSON.stringify(characterData)); + + fetchMock.mockOnceIf(urlFetchById, () => + Promise.resolve({ + status: 200, + body: JSON.stringify(characterData), + }) + ); + const { result } = renderHook(() => useFetchByIdQuery(characterId), { + wrapper, + }); + + expect(result.current).toMatchObject({ + status: 'pending', + endpointName, + isLoading: true, + isSuccess: false, + isError: false, + }); + await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1)); + const calledRequest = fetchMock.mock.calls[0][0]; + + const calledUrl = + calledRequest instanceof Request ? calledRequest.url : calledRequest; + expect(calledUrl).toBe(urlFetchById); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(result.current).toMatchObject({ + status: 'fulfilled', + endpointName, + data: characterData, + isLoading: false, + isSuccess: true, + isError: false, + currentData: characterData, + isFetching: false, + }); + }); +}); diff --git a/src/index.scss b/src/styles/globals.scss similarity index 94% rename from src/index.scss rename to src/styles/globals.scss index 38223ef..efafaac 100644 --- a/src/index.scss +++ b/src/styles/globals.scss @@ -1,4 +1,4 @@ -@use './styles/colors.scss' as *; +@use './colors.scss' as *; body { margin: 0; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts deleted file mode 100644 index 11f02fe..0000000 --- a/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/tsconfig.json b/tsconfig.json index 1ffef60..4c44351 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,32 @@ { - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "incremental": true, + "plugins": [{ "name": "next" }] + }, + "include": [ + "./src", + "./dist/types/**/*.ts", + "./next-env.d.ts", + "./declarations.d.ts" + ], + "exclude": ["./node_modules"] } diff --git a/tsconfig.node.json b/tsconfig.node.json deleted file mode 100644 index db0becc..0000000 --- a/tsconfig.node.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "ES2022", - "lib": ["ES2023"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/vite.config.ts b/vite.config.ts deleted file mode 100644 index 6da11b5..0000000 --- a/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react-swc'; - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [react()], -});