diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..ae78aa74 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# TheCatAPI Configuration +# Get your free API key at https://thecatapi.com/signup + +VITE_CAT_API_KEY=your_api_key_here +VITE_CAT_API_BASE_URL=https://api.thecatapi.com/v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e59e80a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Environment variables +.env +.env.local +.env.production + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..e69de29b diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..3171de5a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/README.md b/README.md index 91d4d5b9..4b6a1b46 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,271 @@ -# GlobalWebIndex Engineering Challenge +# ๐Ÿฑ Cat Lover - React Cat Gallery Application -## Exercise: CatLover +

+ cat_lover_breed_modal + cat_lover_cat_modal + cat_lover_analytics +

-Create a React application for cat lovers which is going to build upon thecatapi.com and will have 3 views. -The **first** view displays a list of 10 random cat images and a button to load more. Clicking on any of those images opens a modal view with the image and the information about the catโ€™s breed if available. This would be a link to the second view below - the breed detail. The modal should also contain a form to mark the image as your favourite (a part of the third view as well). Make sure you can copy-paste the URL of the modal and send it to your friends - they should see the same image as you can see. -The **second** view displays a list of cat breeds. Each breed opens a modal again with a list of cat images of that breed. Each of those images must be a link to the image detail from the previous point. -The **third** view allows you do the following things: +**A React/TypeScript application for cat enthusiasts built with TheCatAPI** -- Display your favourite cats -- Remove an image from your favourites (use any UX option you like) +> ๐ŸŽฎ **[Live Demo](https://mkour-cat-lover.vercel.app/)** | ๐Ÿ”„ **Smart Caching** | ๐Ÿ“Š **Analytics Dashboard** | ๐Ÿ”— **Shareable URLs** -You can find the API documentation here: https://developers.thecatapi.com/ -We give you a lot of freedom in technologies and ways of doing things. We only insist on you using React.js. Get creative as much as you want, we WILL appreciate it. You will not be evaluated based on how well you follow these instructions, but based on how sensible your solution will be. In case you are not able to implement something you would normally implement for time reasons, make it clear with a comment. +
-## Submission + -Once you have built your app, share your code in the mean suits you best -Good luck, potential colleague! +
+ +## โœ… Challenge Requirements Completed + +| Requirement | Status | Implementation | +| ----------------------------- | ------ | ---------------------------------------------- | +| **Random Cat Gallery** | โœ… | 10 cats per load with infinite loading | +| **Shareable Image Modals** | โœ… | Copy-paste URLs that maintain image state | +| **Breed Information Display** | โœ… | Full breed details with temperament and origin | +| **Breed List View** | โœ… | Complete catalogue with images | +| **Favorites Management** | โœ… | Add/remove favorites from gallery or modal | + +## ๐ŸŽฏ Extra Features (Beyond Requirements) + +### Analytics Dashboard + +A complete analytics view tracking user behavior: + +- **Gallery Statistics**: Total cats loaded +- **Breed Views**: Most popular breeds viewed +- **Favorites Tracking**: Comparison between views and favorites +- **Visual Charts**: Bar charts and pie charts using Recharts + +### Smart Breed Caching + +Intelligent data management for optimal performance: + +- **24-Hour Cache**: Breed data fetched once per day +- **Timestamp Validation**: Automatic cache invalidation after 24 hours +- **LocalStorage Integration**: Persistent cache across browser sessions +- **Efficient API Usage**: Minimizes unnecessary API calls + +### Dual Favorite Actions + +Users can favorite cats in two ways: + +1. **From Gallery**: Quick-add button on each card +2. **From Modal**: Expanded favorite button with visual feedback + +## Getting Started + +```bash +# Clone the repository +git clone https://github.com/mkourogiorgas/cat-lover.git + +# Navigate to project directory +cd cat-lover + +# Install dependencies +npm install + +# Create .env file with your API key +echo "VITE_CAT_API_KEY=your_api_key_here" > .env + +# Start development server +npm run dev +``` + +**Requirements:** + +- Node.js 18+ +- npm 9+ +- TheCatAPI key (free at [thecatapi.com](https://thecatapi.com/)) + +## Technical Stack + +| Technology | Purpose | Version | +| ----------------- | -------------------- | ------- | +| **React** | UI Framework | 19.2.0 | +| **TypeScript** | Type Safety | 5.9.3 | +| **Vite** | Build Tool | 7.2.2 | +| **Redux Toolkit** | State Management | 2.10.1 | +| **React Router** | Navigation & Routing | 7.9.5 | +| **Axios** | HTTP Client | 1.13.2 | +| **Recharts** | Data Visualization | 3.4.1 | +| **CSS Modules** | Scoped Styling | Native | +| **Vitest** | Testing Framework | 4.0.8 | + +## Architecture Highlights + +### State Management Strategy + +``` +Redux Store +โ”œโ”€โ”€ gallerySlice โ†’ Random cat images (normalized by ID) +โ”œโ”€โ”€ breedsSlice โ†’ Breed catalog with timestamp metadata +โ”œโ”€โ”€ favouritesSlice โ†’ User favorites (persisted to localStorage) +โ””โ”€โ”€ analyticsSlice โ†’ User interaction tracking +``` + +### Smart Caching Logic + +**Why This Matters:** + +- โœ… Reduces API calls (breed data rarely changes) +- โœ… Faster load times (data served from localStorage) +- โœ… Better UX (instant navigation between views) + +### Project Structure + +``` +src/ +โ”œโ”€โ”€ api/ # API client and services +โ”‚ โ”œโ”€โ”€ api.ts # Axios instance with interceptors +โ”‚ โ”œโ”€โ”€ services.ts # API methods (fetchCats, fetchBreeds) +โ”‚ โ””โ”€โ”€ endpoints.ts # API endpoint constants +โ”œโ”€โ”€ components/ # Reusable UI components +โ”‚ โ”œโ”€โ”€ card/ # Cat card with image and favorite button +โ”‚ โ”œโ”€โ”€ modal/ # Image detail modal (shareable) +โ”‚ โ”œโ”€โ”€ breedModal/ # Breed detail modal with image grid +โ”‚ โ”œโ”€โ”€ navigation/ # Top navigation bar +โ”‚ โ””โ”€โ”€ ... +โ”œโ”€โ”€ views/ # Page-level components +โ”‚ โ”œโ”€โ”€ gallery/ # Random cat images view +โ”‚ โ”œโ”€โ”€ breeds/ # Breed catalog view +โ”‚ โ”œโ”€โ”€ favourites/ # User favorites view +โ”‚ โ””โ”€โ”€ analytics/ # Analytics dashboard +โ”œโ”€โ”€ store/ # Redux state management +โ”‚ โ”œโ”€โ”€ gallerySlice.ts +โ”‚ โ”œโ”€โ”€ breedsSlice.ts +โ”‚ โ”œโ”€โ”€ favouritesSlice.ts +โ”‚ โ””โ”€โ”€ analyticsSlice.ts +โ”œโ”€โ”€ hooks/ # Custom React hooks +โ”‚ โ”œโ”€โ”€ useBreedsPersistence.ts +โ”‚ โ”œโ”€โ”€ useFavouritesPersistence.ts +โ”‚ โ””โ”€โ”€ useAnalyticsPersistence.ts +โ””โ”€โ”€ types.ts # TypeScript type definitions +``` + +## Key Features in Detail + +### 1. Shareable Modal URLs + +Each cat image has a unique URL that can be shared: + +``` +/cat/j6oFGLpRG โ†’ Opens modal in gallery view +/breeds/cat/j6oFGLpRG โ†’ Opens modal in breeds view +/favourites/cat/j6oFGLpRG โ†’ Opens modal in favourites view +``` + +Uses React Router nested routes for clean URL structure. + +### 2. Persistent Favorites + +Favorites are saved to `localStorage` and restored on page load: + +- Survives browser refresh +- Works offline +- Synced with Redux state + +### 3. Analytics Tracking + +Automatically tracks: + +- Number of cats loaded in gallery +- Which breeds are viewed most often +- Favorite/view ratios + +### 4. Optimistic UI Updates + +Favorite button responds instantly without waiting for state updates, providing a smooth user experience. + +## Development + +```bash +# Run tests +npm run test + +# Lint code +npm run lint + +# Build for production +npm run build + +# Preview production build +npm run preview +``` + +## Testing + +Unit tests cover: + +- Redux slice reducers (gallery, breeds, favourites, analytics) +- Utility functions (mostly edge cases and error-prone utility functions) + +```bash +# Run all tests +npm run test + +``` + +## Deployment + +This project is configured for easy deployment on Vercel: + +1. Push to GitHub +2. Import project in Vercel +3. Add `VITE_CAT_API_KEY` environment variable +4. Deploy + +## Browser Support + +- Chrome/Edge 90+ +- Firefox 88+ +- Safari 15+ + +## API Usage + +This application uses [TheCatAPI](https://thecatapi.com/): + +- **Random Images**: `/v1/images/search` +- **Breeds List**: `/v1/breeds` +- **Image by ID**: `/v1/images/{id}` +- **Breed Images**: `/v1/images/search?breed_ids={id}` + +## Project Decisions + +### Why Redux Toolkit? + +- Centralized state management for complex interactions +- Easy integration with localStorage persistence +- Excellent TypeScript support +- Built-in performance optimizations + +### Why CSS Modules? + +- Scoped styles prevent naming conflicts +- Better tree-shaking than global CSS +- Type-safe with TypeScript + +### Why 24-Hour Cache? + +- Breed data is static and rarely changes +- Reduces API load and costs +- Improves application responsiveness +- Balances freshness with performance + +### Why Recharts? + +- React-native charting library +- Responsive out of the box +- Simple API for common chart types +- Good TypeScript support + +## License + +This project is licensed under the MIT License. + +## Acknowledgments + +- [TheCatAPI](https://thecatapi.com/) for providing the cat data +- Challenge provided by GlobalWebIndex Engineering diff --git a/__tests__/unit/store/analyticsSlice.test.ts b/__tests__/unit/store/analyticsSlice.test.ts new file mode 100644 index 00000000..54c9e1b1 --- /dev/null +++ b/__tests__/unit/store/analyticsSlice.test.ts @@ -0,0 +1,125 @@ +import { describe, expect,it } from 'vitest'; +import analyticsReducer, { + analyticsActions, +} from '../../../src/store/analyticsSlice'; + +import type { AnalyticsState } from '../../../src/store/types'; + +const { + incrementBreedView, + incrementGalleryCats, + resetAnalytics, + loadAnalytics, +} = analyticsActions; + +describe('analyticsSlice', () => { + const initialState: AnalyticsState = { + galleryCats: 0, + breedsViewed: {}, + }; + + describe('incrementGalleryCats', () => { + it('should increment gallery cats count by the number given', () => { + const state = { ...initialState }; + const action = incrementGalleryCats(8); + const newState = analyticsReducer(state, action); + expect(newState.galleryCats).toBe(8); + }); + + it('should add to existing gallery cats count', () => { + const state: AnalyticsState = { galleryCats: 5, breedsViewed: {} }; + const action = incrementGalleryCats(10); + const newState = analyticsReducer(state, action); + expect(newState.galleryCats).toBe(15); + }); + }); + + describe('incrementBreedView', () => { + it('should add new breed to breedsViewed', () => { + const state = { ...initialState }; + const action = incrementBreedView({ breedName: 'Randrom Breed Name' }); + const newState = analyticsReducer(state, action); + expect(newState.breedsViewed['Randrom Breed Name']).toBe(1); + }); + + it('should increment existing breed view count', () => { + const state: AnalyticsState = { + galleryCats: 0, + breedsViewed: { 'Random Breed Name': 2 }, + }; + const action = incrementBreedView({ breedName: 'Random Breed Name' }); + const newState = analyticsReducer(state, action); + expect(newState.breedsViewed['Random Breed Name']).toBe(3); + }); + + it('should track multiple different breeds and increment correctly all of them', () => { + let state: AnalyticsState = { ...initialState }; + state = analyticsReducer( + state, + incrementBreedView({ breedName: 'Breed 2' }) + ); + state = analyticsReducer( + state, + incrementBreedView({ breedName: 'Breed 1' }) + ); + state = analyticsReducer( + state, + incrementBreedView({ breedName: 'Breed 2' }) + ); + + expect(state.breedsViewed['Breed 2']).toBe(2); + expect(state.breedsViewed['Breed 1']).toBe(1); + }); + + it('should handle breed names with spaces', () => { + const state = { ...initialState }; + const action = incrementBreedView({ breedName: 'Maine Coon' }); + const newState = analyticsReducer(state, action); + expect(newState.breedsViewed['Maine Coon']).toBe(1); + }); + }); + + describe('resetAnalytics', () => { + it('should reset analytics to initial state', () => { + const state: AnalyticsState = { + galleryCats: 100, + breedsViewed: { Siamese: 10, Persian: 5 }, + }; + const action = resetAnalytics(); + const newState = analyticsReducer(state, action); + expect(newState.galleryCats).toBe(0); + expect(newState.breedsViewed).toEqual({}); + }); + }); + + describe('loadAnalytics', () => { + it('should load analytics state from payload', () => { + const state = { ...initialState }; + const payload: AnalyticsState = { + galleryCats: 22, + breedsViewed: { 'Random Breed': 15 }, + }; + const action = loadAnalytics(payload); + const newState = analyticsReducer(state, action); + expect(newState.galleryCats).toBe(22); + expect(newState.breedsViewed['Random Breed']).toBe(15); + }); + + it('should replace entire analytics state', () => { + const state: AnalyticsState = { + galleryCats: 100, + breedsViewed: { Persian: 10 }, + }; + const payload: AnalyticsState = { + galleryCats: 25, + breedsViewed: { Siamese: 8, Bengal: 3 }, + }; + const action = loadAnalytics(payload); + const newState = analyticsReducer(state, action); + expect(newState.galleryCats).toBe(25); + expect(newState.breedsViewed['Persian']).toBeUndefined(); + expect(newState.breedsViewed['Siamese']).toBe(8); + expect(newState.breedsViewed['Bengal']).toBe(3); + }); + }); +}); diff --git a/__tests__/unit/store/breedsSlice.test.ts b/__tests__/unit/store/breedsSlice.test.ts new file mode 100644 index 00000000..044b8514 --- /dev/null +++ b/__tests__/unit/store/breedsSlice.test.ts @@ -0,0 +1,187 @@ +import { describe, expect,it } from 'vitest'; +import breedsReducer, { + breedsActions, + type BreedsStateWithMeta, +} from '../../../src/store/breedsSlice'; + +import type { Breed } from '../../../src/types'; + +const { addBreeds, clearBreeds, loadBreeds } = breedsActions; + +const mockBreed: Breed = { + id: 'bengal', + name: 'Bengal', + temperament: 'Alert, Agile, Active', + origin: 'United States', + description: 'The Bengal is a leopard cat hybrid', + life_span: '10 - 16', + affection_level: 4, + child_friendly: 3, + dog_friendly: 3, + weight: { + imperial: '8 - 15', + metric: '4 - 7', + }, + image: { + id: 'img1', + url: 'http://example.com/cat.png', + width: 400, + height: 300, + }, +}; + +describe('breedsSlice', () => { + const initialState: BreedsStateWithMeta = { + data: {}, + lastFetched: null, + }; + + describe('addBreeds', () => { + it('should add a single breed', () => { + const state: BreedsStateWithMeta = { ...initialState }; + const action = addBreeds([mockBreed]); + const newState = breedsReducer(state, action); + expect(newState.data['bengal']).toBe(mockBreed); + }); + + it('should add multiple breeds', () => { + const state: BreedsStateWithMeta = { ...initialState }; + const siamese: Breed = { ...mockBreed, id: 'siamese', name: 'Siamese' }; + const persian: Breed = { ...mockBreed, id: 'persian', name: 'Persian' }; + const action = addBreeds([mockBreed, siamese, persian]); + const newState = breedsReducer(state, action); + expect(Object.keys(newState.data)).toHaveLength(3); + expect(newState.data['bengal']).toBe(mockBreed); + expect(newState.data['siamese']).toBe(siamese); + expect(newState.data['persian']).toBe(persian); + }); + + it('should update existing breed when adding duplicate id', () => { + const state: BreedsStateWithMeta = { + data: { bengal: mockBreed }, + lastFetched: Date.now(), + }; + const updatedBreed: Breed = { ...mockBreed, temperament: 'Updated' }; + const action = addBreeds([updatedBreed]); + const newState = breedsReducer(state, action); + expect(newState.data['bengal'].temperament).toBe('Updated'); + }); + + it('should preserve existing breeds when adding new ones', () => { + const siamese: Breed = { ...mockBreed, id: 'siamese', name: 'Siamese' }; + const state: BreedsStateWithMeta = { + data: { siamese: siamese }, + lastFetched: Date.now(), + }; + const action = addBreeds([mockBreed]); + const newState = breedsReducer(state, action); + expect(Object.keys(newState.data)).toHaveLength(2); + expect(newState.data['bengal']).toBe(mockBreed); + expect(newState.data['siamese']).toBe(siamese); + }); + + it('should handle empty breed array', () => { + const state: BreedsStateWithMeta = { + data: { bengal: mockBreed }, + lastFetched: Date.now(), + }; + const action = addBreeds([]); + const newState = breedsReducer(state, action); + expect(newState.data['bengal']).toBe(mockBreed); + }); + + it('should update lastFetched if 24 hours passed', () => { + const ONE_DAY_MS = 24 * 60 * 60 * 1000; + const oldTimestamp = Date.now() - ONE_DAY_MS - 1000; + const state: BreedsStateWithMeta = { + data: {}, + lastFetched: oldTimestamp, + }; + const action = addBreeds([mockBreed]); + const newState = breedsReducer(state, action); + expect(newState.lastFetched).toBeGreaterThan(oldTimestamp); + }); + }); + + describe('clearBreeds', () => { + it('should clear all breeds', () => { + const state: BreedsStateWithMeta = { + data: { + bengal: mockBreed, + siamese: { ...mockBreed, id: 'siamese' }, + }, + lastFetched: Date.now(), + }; + const action = clearBreeds(); + const newState = breedsReducer(state, action); + expect(newState).toEqual(initialState); + }); + + it('should work on empty state', () => { + const state: BreedsStateWithMeta = { ...initialState }; + const action = clearBreeds(); + const newState = breedsReducer(state, action); + expect(newState).toEqual(initialState); + }); + }); + + describe('loadBreeds', () => { + it('should load breeds from payload', () => { + const state: BreedsStateWithMeta = { ...initialState }; + const payload: BreedsStateWithMeta = { + data: { bengal: mockBreed }, + lastFetched: Date.now(), + }; + const action = loadBreeds(payload); + const newState = breedsReducer(state, action); + expect(newState.data['bengal']).toBe(mockBreed); + }); + + it('should replace existing breeds', () => { + const state: BreedsStateWithMeta = { + data: { bengal: mockBreed }, + lastFetched: Date.now(), + }; + const siamese: Breed = { ...mockBreed, id: 'siamese', name: 'Siamese' }; + const payload: BreedsStateWithMeta = { + data: { siamese: siamese }, + lastFetched: Date.now(), + }; + const action = loadBreeds(payload); + const newState = breedsReducer(state, action); + expect(newState.data['bengal']).toBeUndefined(); + expect(newState.data['siamese']).toBe(siamese); + }); + + it('should load multiple breeds at once', () => { + const state: BreedsStateWithMeta = { ...initialState }; + const siamese: Breed = { ...mockBreed, id: 'siamese', name: 'Siamese' }; + const persian: Breed = { ...mockBreed, id: 'persian', name: 'Persian' }; + const payload: BreedsStateWithMeta = { + data: { + bengal: mockBreed, + siamese: siamese, + persian: persian, + }, + lastFetched: Date.now(), + }; + const action = loadBreeds(payload); + const newState = breedsReducer(state, action); + expect(Object.keys(newState.data)).toHaveLength(3); + }); + + it('should load empty state', () => { + const state: BreedsStateWithMeta = { + data: { bengal: mockBreed }, + lastFetched: Date.now(), + }; + const payload: BreedsStateWithMeta = { + data: {}, + lastFetched: null, + }; + const action = loadBreeds(payload); + const newState = breedsReducer(state, action); + expect(newState).toEqual(payload); + }); + }); +}); diff --git a/__tests__/unit/store/favouritesSlice.test.ts b/__tests__/unit/store/favouritesSlice.test.ts new file mode 100644 index 00000000..d60220bf --- /dev/null +++ b/__tests__/unit/store/favouritesSlice.test.ts @@ -0,0 +1,80 @@ +import { describe, expect,it } from 'vitest'; +import favouritesReducer, { + favouritesActions, +} from '../../../src/store/favouritesSlice'; + +import type { FavouritesState } from '../../../src/store/types'; +import type { Cat } from '../../../src/types'; + +const { toggleFavourite, clearFavourites, loadFavourites } = favouritesActions; + +const mockCat: Cat = { + id: 'cat1', + url: 'http://example.com/cat1.jpg', + width: 400, + height: 300, + breeds: [], +}; + +describe('favouritesSlice', () => { + const initialState: FavouritesState = {}; + + describe('toggleFavourite', () => { + it('should add a cat to favourites', () => { + const state: FavouritesState = { ...initialState }; + const action = toggleFavourite(mockCat); + const newState = favouritesReducer(state, action); + expect(newState['cat1']).toBe(mockCat); + }); + + it('should remove a cat from favourites', () => { + const state: FavouritesState = { cat1: mockCat }; + const action = toggleFavourite(mockCat); + const newState = favouritesReducer(state, action); + expect(newState['cat1']).toBeUndefined(); + }); + }); + + describe('clearFavourites', () => { + it('should work on empty state', () => { + const state: FavouritesState = { ...initialState }; + const action = clearFavourites(); + const newState = favouritesReducer(state, action); + expect(newState).toEqual({}); + }); + }); + + describe('loadFavourites', () => { + it('should load favourites from payload', () => { + const state: FavouritesState = { ...initialState }; + const payload: FavouritesState = { cat1: mockCat }; + const action = loadFavourites(payload); + const newState = favouritesReducer(state, action); + expect(newState['cat1']).toBe(mockCat); + }); + + it('should replace existing favourites', () => { + const state: FavouritesState = { cat1: mockCat }; + const cat2: Cat = { ...mockCat, id: 'cat2' }; + const payload: FavouritesState = { cat2: cat2 }; + const action = loadFavourites(payload); + const newState = favouritesReducer(state, action); + expect(newState['cat1']).toBeUndefined(); + expect(newState['cat2']).toBe(cat2); + }); + + it('should load multiple favourites at once', () => { + const state: FavouritesState = { ...initialState }; + const cat2: Cat = { ...mockCat, id: 'cat2' }; + const cat3: Cat = { ...mockCat, id: 'cat3' }; + const payload: FavouritesState = { + cat1: mockCat, + cat2: cat2, + cat3: cat3, + }; + const action = loadFavourites(payload); + const newState = favouritesReducer(state, action); + expect(Object.keys(newState)).toHaveLength(3); + }); + }); +}); diff --git a/__tests__/unit/utils/breeds.test.ts b/__tests__/unit/utils/breeds.test.ts new file mode 100644 index 00000000..79366c1a --- /dev/null +++ b/__tests__/unit/utils/breeds.test.ts @@ -0,0 +1,41 @@ +import { describe, expect,it } from 'vitest'; + +import U from '../../../src/views/breeds/utils'; + +describe('breeds utils fetch', () => { + describe('shouldFetchBreeds', () => { + it('should return true when data is empty', () => { + const result = U.shouldFetchBreeds(true, null); + expect(result).toBe(true); + }); + + it('should return true when lastFetched is null', () => { + const result = U.shouldFetchBreeds(false, null); + expect(result).toBe(true); + }); + + it('should return false when cache is newer than 24 hours ago', () => { + const oneHourAgo = Date.now() - 60 * 60 * 1000; + const result = U.shouldFetchBreeds(false, oneHourAgo); + expect(result).toBe(false); + }); + + it('should return true when cache is older than 1 day', () => { + const twentyFiveHoursAgo = Date.now() - 25 * 60 * 60 * 1000; + const result = U.shouldFetchBreeds(false, twentyFiveHoursAgo); + expect(result).toBe(true); + }); + + it('should return true when cache is exactly 1 day old', () => { + const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000; + const result = U.shouldFetchBreeds(false, oneDayAgo); + expect(result).toBe(true); + }); + + it('should handle empty data and old cache', () => { + const oldTimestamp = Date.now() - 48 * 60 * 60 * 1000; + const result = U.shouldFetchBreeds(true, oldTimestamp); + expect(result).toBe(true); + }); + }); +}); diff --git a/__tests__/unit/utils/graphs.test.ts b/__tests__/unit/utils/graphs.test.ts new file mode 100644 index 00000000..0f723c7b --- /dev/null +++ b/__tests__/unit/utils/graphs.test.ts @@ -0,0 +1,159 @@ +import { describe, expect,it } from 'vitest'; + +import U from '../../../src/views/analytics/utils'; + +const { formatStats, formatBarStats, formatPieChartData } = U; + +describe('Analytics Stats', () => { + describe('formatStats', () => { + it('should format stats with correct structure', () => { + const result = formatStats(50, 25, '50.0'); + expect(result).toHaveLength(3); + expect(result[0]).toMatchObject({ + label: expect.any(String), + value: expect.any(Number), + color: expect.any(String), + }); + }); + + it('should include total images viewed stat', () => { + const result = formatStats(100, 25, '25.0'); + expect(result[0].value).toBe(100); + }); + + it('should include total favourites stat', () => { + const result = formatStats(100, 25, '25.0'); + expect(result[1].value).toBe(25); + }); + + it('should include favourite rate stat', () => { + const result = formatStats(100, 25, '25.0'); + expect(result[2].value).toBe('25.0%'); + }); + + it('should handle zero values', () => { + const result = formatStats(0, 0, '0'); + expect(result[0].value).toBe(0); + expect(result[1].value).toBe(0); + expect(result[2].value).toBe('0%'); + }); + + it('should handle high values', () => { + const result = formatStats(10000, 5000, '50.0'); + expect(result[0].value).toBe(10000); + expect(result[1].value).toBe(5000); + }); + }); + + describe('formatBarStats', () => { + it('should format bar stats with correct structure', () => { + const result = formatBarStats(100, 25); + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + name: expect.any(String), + Views: expect.any(Number), + Favourites: expect.any(Number), + }); + }); + + it('should include views count', () => { + const result = formatBarStats(150, 50); + expect(result[0].Views).toBe(150); + }); + + it('should include favourites count', () => { + const result = formatBarStats(150, 50); + expect(result[0].Favourites).toBe(50); + }); + + it('should have name property for chart', () => { + const result = formatBarStats(100, 25); + expect(result[0].name).toBe('Total'); + }); + + it('should handle zero values', () => { + const result = formatBarStats(0, 0); + expect(result[0].Views).toBe(0); + expect(result[0].Favourites).toBe(0); + }); + }); + + describe('formatPieChartData', () => { + it('should return empty array when breedsViewed is undefined', () => { + const result = formatPieChartData(undefined); + expect(result).toEqual([]); + }); + + it('should return empty array when breedsViewed is empty', () => { + const result = formatPieChartData({}); + expect(result).toEqual([]); + }); + + it('should format single breed correctly', () => { + const breedsViewed = { Siamese: 5 }; + const result = formatPieChartData(breedsViewed); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + name: 'Siamese', + value: 5, + }); + }); + + it('should format multiple breeds correctly', () => { + const breedsViewed = { + Siamese: 10, + Persian: 8, + 'Maine Coon': 6, + }; + const result = formatPieChartData(breedsViewed); + + expect(result).toHaveLength(3); + }); + + it('should sort breeds by view count (descending)', () => { + const breedsViewed = { + Siamese: 5, + Persian: 15, + 'Maine Coon': 10, + }; + const result = formatPieChartData(breedsViewed); + + expect(result[0].name).toBe('Persian'); + expect(result[0].value).toBe(15); + expect(result[1].name).toBe('Maine Coon'); + expect(result[2].name).toBe('Siamese'); + }); + + it('should limit to top 6 breeds', () => { + const breedsViewed = { + Breed1: 10, + Breed2: 9, + Breed3: 8, + Breed4: 7, + Breed5: 6, + Breed6: 5, + Breed7: 4, + Breed8: 3, + }; + const result = formatPieChartData(breedsViewed); + + expect(result).toHaveLength(6); + expect(result[result.length - 1].name).toBe('Breed6'); + }); + + it('should handle breeds with same view count', () => { + const breedsViewed = { + Siamese: 10, + Persian: 10, + 'Maine Coon': 10, + }; + const result = formatPieChartData(breedsViewed); + + expect(result).toHaveLength(3); + expect(result.every((item: { value: number }) => item.value === 10)).toBe( + true + ); + }); + }); +}); diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..0efc9b37 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,59 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import simpleImportSort from 'eslint-plugin-simple-import-sort'; + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'simple-import-sort': simpleImportSort, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + 'simple-import-sort/imports': [ + 'error', + { + groups: [ + // 1. React & React Router + ['^react', '^react-router'], + // 2. Third-party libraries & Relative imports (no empty line between) + [ + '^[^./@]', + '^@(?!.*/types)', + '^\\./(?!(?:utils|constants))', + '^\\.\\./(?!(?:utils|constants))', + ], + // 3. Utils, Constants, CSS modules, Types (all together after empty line) + [ + 'utils', + 'constants', + '\\.module\\.css$', + '^.*/types', + '^@.*/types', + ], + ], + }, + ], + 'simple-import-sort/exports': 'error', + }, + }, +]); diff --git a/index.html b/index.html new file mode 100644 index 00000000..d667825e --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + + Cat Lovers + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..8cb8ea65 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5483 @@ +{ + "name": "cat-lover", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cat-lover", + "version": "0.0.0", + "dependencies": { + "@reduxjs/toolkit": "^2.10.1", + "axios": "^1.13.2", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-redux": "^9.2.0", + "react-router-dom": "^7.9.5", + "recharts": "^3.4.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^24.10.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.1.0", + "@vitest/ui": "^4.0.8", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "eslint-plugin-simple-import-sort": "^12.1.1", + "globals": "^16.5.0", + "jsdom": "^27.2.0", + "prettier": "^3.6.2", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.3", + "vite": "^7.2.2", + "vitest": "^4.0.8" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.23", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.23.tgz", + "integrity": "sha512-2kJ1HxBKzPLbmhZpxBiTZggjtgCwKg1ma5RHShxvd6zgqhDEdEkzpiwe7jLkI2p2BrZvFCXIihdoMkl1H39VnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz", + "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.1" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.4.tgz", + "integrity": "sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.2" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.16.tgz", + "integrity": "sha512-2SpS4/UaWQaGpBINyG5ZuCHnUDeVByOhvbkARwfmnfxDvTaj80yOI1cD8Tw93ICV5Fx4fnyDKWQZI1CDtcWyUg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "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/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.1.tgz", + "integrity": "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.2.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz", + "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz", + "integrity": "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/type-utils": "8.46.4", + "@typescript-eslint/utils": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.4", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.4.tgz", + "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", + "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.4", + "@typescript-eslint/types": "^8.46.4", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", + "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", + "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz", + "integrity": "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", + "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", + "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.4", + "@typescript-eslint/tsconfig-utils": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz", + "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", + "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.4", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", + "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.47", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.9.tgz", + "integrity": "sha512-C2vyXf5/Jfj1vl4DQYxjib3jzyuswMi/KHHVN2z+H4v16hdJ7jMZ0OGe3uOVIt6LyJsAofDdaJNIFEpQcrSTFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.9", + "@vitest/utils": "4.0.9", + "chai": "^6.2.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.9.tgz", + "integrity": "sha512-PUyaowQFHW+9FKb4dsvvBM4o025rWMlEDXdWRxIOilGaHREYTi5Q2Rt9VCgXgPy/hHZu1LeuXtrA/GdzOatP2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.9.tgz", + "integrity": "sha512-Hor0IBTwEi/uZqB7pvGepyElaM8J75pYjrrqbC8ZYMB9/4n5QA63KC15xhT+sqHpdGWfdnPo96E8lQUxs2YzSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.9.tgz", + "integrity": "sha512-aF77tsXdEvIJRkj9uJZnHtovsVIx22Ambft9HudC+XuG/on1NY/bf5dlDti1N35eJT+QZLb4RF/5dTIG18s98w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.9.tgz", + "integrity": "sha512-r1qR4oYstPbnOjg0Vgd3E8ADJbi4ditCzqr+Z9foUrRhIy778BleNyZMeAJ2EjV+r4ASAaDsdciC9ryMy8xMMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.9.tgz", + "integrity": "sha512-J9Ttsq0hDXmxmT8CUOWUr1cqqAj2FJRGTdyEjSR+NjoOGKEqkEWj+09yC0HhI8t1W6t4Ctqawl1onHgipJve1A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.9.tgz", + "integrity": "sha512-6HV2HHl9aRJ09TlYj/WAQxaa797Ezb5u0LpgabthlASAUAWKgw/W1DSPX7t848mMZmIUvzZgnUHGIylAoYHP0w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/utils": "4.0.9", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.9" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.9.tgz", + "integrity": "sha512-cEol6ygTzY4rUPvNZM19sDf7zGa35IYTm9wfzkHoT/f5jX10IOY7QleWSOh5T0e3I3WVozwK5Asom79qW8DiuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.9", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz", + "integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001755", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", + "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", + "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "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, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.3.tgz", + "integrity": "sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.0.3", + "@csstools/css-syntax-patches-for-csstree": "^1.0.14", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/csstype": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.2.tgz", + "integrity": "sha512-D80T+tiqkd/8B0xNlbstWDG4x6aqVfO52+OlSUNIdkTvmNw0uQpJLeos2J/2XvpyidAFuTPmpad+tUxLndwj6g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.254", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz", + "integrity": "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-toolkit": { + "version": "1.41.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.41.0.tgz", + "integrity": "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-plugin-simple-import-sort": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", + "integrity": "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=5.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "27.2.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.2.0.tgz", + "integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@acemir/cssom": "^0.9.23", + "@asamuzakjp/dom-selector": "^6.7.4", + "cssstyle": "^5.3.3", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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.11", + "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", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-is": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz", + "integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.6.tgz", + "integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.6" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/recharts": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.4.1.tgz", + "integrity": "sha512-35kYg6JoOgwq8sE4rhYkVWwa6aAIgOtT+Ob0gitnShjwUwZmhrmy7Jco/5kJNF4PnLXgt9Hwq+geEMS+WrjU1g==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "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" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", + "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.17" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", + "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.4.tgz", + "integrity": "sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.4", + "@typescript-eslint/parser": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "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/vitest": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.9.tgz", + "integrity": "sha512-E0Ja2AX4th+CG33yAFRC+d1wFx2pzU5r6HtG6LiPSE04flaE0qB6YyjSw9ZcpJAtVPfsvZGtJlKWZpuW7EHRxg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/expect": "4.0.9", + "@vitest/mocker": "4.0.9", + "@vitest/pretty-format": "4.0.9", + "@vitest/runner": "4.0.9", + "@vitest/snapshot": "4.0.9", + "@vitest/spy": "4.0.9", + "@vitest/utils": "4.0.9", + "debug": "^4.4.3", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.9", + "@vitest/browser-preview": "4.0.9", + "@vitest/browser-webdriverio": "4.0.9", + "@vitest/ui": "4.0.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..b43001f4 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "cat-lover", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "test": "vitest" + }, + "dependencies": { + "@reduxjs/toolkit": "^2.10.1", + "axios": "^1.13.2", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-redux": "^9.2.0", + "react-router-dom": "^7.9.5", + "recharts": "^3.4.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^24.10.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.1.0", + "@vitest/ui": "^4.0.8", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "eslint-plugin-simple-import-sort": "^12.1.1", + "globals": "^16.5.0", + "jsdom": "^27.2.0", + "prettier": "^3.6.2", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.3", + "vite": "^7.2.2", + "vitest": "^4.0.8" + } +} diff --git a/public/cat.png b/public/cat.png new file mode 100644 index 00000000..3616bb9e Binary files /dev/null and b/public/cat.png differ diff --git a/public/favicon-dark.png b/public/favicon-dark.png new file mode 100644 index 00000000..ba0028d0 Binary files /dev/null and b/public/favicon-dark.png differ diff --git a/public/favicon-light.png b/public/favicon-light.png new file mode 100644 index 00000000..ff1803f7 Binary files /dev/null and b/public/favicon-light.png differ diff --git a/src/App.css b/src/App.css new file mode 100644 index 00000000..6b9dc2e8 --- /dev/null +++ b/src/App.css @@ -0,0 +1,12 @@ +.app { + min-height: 100vh; + display: flex; + flex-direction: column; + background: #fef9f0; + width: 100%; +} + +.main { + flex: 1; + background: #fef9f0; +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 00000000..67776061 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,25 @@ +import './App.css'; +import Navigation from './components/navigation'; +import { + useAnalyticsPersistence, + useBreedsPersistence, + useFavouritesPersistence, +} from './hooks'; +import AppRouter from './router'; + +const App = () => { + useAnalyticsPersistence(); + useBreedsPersistence(); + useFavouritesPersistence(); + + return ( +
+ +
+ +
+
+ ); +}; + +export default App; diff --git a/src/api/api.ts b/src/api/api.ts new file mode 100644 index 00000000..d677c78a --- /dev/null +++ b/src/api/api.ts @@ -0,0 +1,31 @@ +import axios from 'axios'; + +import C from './constants'; + +const API_KEY = import.meta.env.VITE_CAT_API_KEY; +const API_BASE_URL = + import.meta.env.VITE_CAT_API_BASE_URL || 'https://api.thecatapi.com/v1'; + +const api = axios.create({ + baseURL: API_BASE_URL, + headers: { + 'x-api-key': API_KEY || '', + }, + timeout: 10000, +}); + +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response) { + console.error(C.API_ERROR, error.response.status, error.response.data); + } else if (error.request) { + console.error(C.NETWORK_ERROR); + } else { + console.error(C.REQUEST_ERROR, error.message); + } + return Promise.reject(error); + } +); + +export default api; diff --git a/src/api/constants.ts b/src/api/constants.ts new file mode 100644 index 00000000..34d0c6d4 --- /dev/null +++ b/src/api/constants.ts @@ -0,0 +1,13 @@ +const API_ERROR = 'API Error:'; +const NETWORK_ERROR = 'Network Error: No response from server'; +const REQUEST_ERROR = 'Request Error:'; +const DEFAULT_CATS_LIMIT = 3; +const DEFAULT_BREED_IMAGES_LIMIT = 4; + +export default { + API_ERROR, + DEFAULT_CATS_LIMIT, + DEFAULT_BREED_IMAGES_LIMIT, + NETWORK_ERROR, + REQUEST_ERROR, +}; diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts new file mode 100644 index 00000000..f67d1090 --- /dev/null +++ b/src/api/endpoints.ts @@ -0,0 +1,8 @@ +const endpoints = { + IMAGES: '/images/search' as const, + IMAGE_BY_ID: (imageId: string): string => `/images/${imageId}`, + BREEDS: '/breeds' as const, + BREED_BY_ID: (breedId: string): string => `/breeds/${breedId}`, +} as const; + +export default endpoints; diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 00000000..e371345e --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1 @@ +export * from './services'; diff --git a/src/api/services.ts b/src/api/services.ts new file mode 100644 index 00000000..4ec0df09 --- /dev/null +++ b/src/api/services.ts @@ -0,0 +1,47 @@ +import api from './api'; +import endpoints from './endpoints'; + +import C from './constants'; +import type { Breed, Cat } from '../types'; + +const fetchRandomCats = async ( + limit: number = C.DEFAULT_CATS_LIMIT +): Promise => { + const response = await api.get(endpoints.IMAGES, { + params: { + limit, + has_breeds: true, + }, + }); + return response.data; +}; + +const fetchCatImageById = async (imageId: string): Promise => { + const response = await api.get(endpoints.IMAGE_BY_ID(imageId)); + return response.data; +}; + +const fetchAllBreeds = async (): Promise => { + const response = await api.get(endpoints.BREEDS); + return response.data; +}; + +const fetchImagesByBreed = async ( + breedId: string, + limit: number = C.DEFAULT_CATS_LIMIT +): Promise => { + const response = await api.get(endpoints.IMAGES, { + params: { + breed_ids: breedId, + limit, + }, + }); + return response.data; +}; + +export { + fetchAllBreeds, + fetchCatImageById, + fetchImagesByBreed, + fetchRandomCats, +}; diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/breedModal/BreedModal.module.css b/src/components/breedModal/BreedModal.module.css new file mode 100644 index 00000000..dabe067e --- /dev/null +++ b/src/components/breedModal/BreedModal.module.css @@ -0,0 +1,256 @@ +.overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(61, 40, 23, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 2rem; + backdrop-filter: blur(4px); + overflow: hidden; +} + +html:has(.overlay) { + overflow: hidden; +} + +.modal { + background: #fff; + border-radius: 1rem; + max-width: 800px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + position: relative; + border: 3px solid #3d2817; + box-shadow: 0 8px 32px rgba(61, 40, 23, 0.3); + padding: 3rem; +} + +.closeButton { + position: absolute; + top: 1rem; + right: 1rem; + background: none; + border: none; + border-radius: 0; + width: auto; + height: auto; + font-size: 2rem; + cursor: pointer; + color: #3d2817; + font-weight: bold; + z-index: 10; + padding: 0; + line-height: 1; +} + +.closeButton:hover { + color: #ff6b9d; + background: none; + transform: none; +} + +.content { + width: 100%; +} + +.breedDetails { + text-align: center; + margin-bottom: 2rem; +} + +.breedHeader { + margin-bottom: 1.5rem; +} + +.breedName { + margin: 0 0 0.5rem 0; + font-size: 2.5rem; + font-weight: 800; + color: #3d2817; +} + +.breedOrigin { + margin: 0; + font-size: 1.125rem; + color: #5a4a3a; + font-weight: 600; +} + +.breedDescription { + margin: 0 0 2rem 0; + font-size: 1rem; + color: #5a4a3a; + line-height: 1.6; + text-align: center; +} + +.detailsGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + background: #fce8b8; + padding: 1.5rem; + border-radius: 0.75rem; + text-align: left; +} + +.detailItem { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 0.9375rem; + color: #3d2817; +} + +.detailItem strong { + font-weight: 700; + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #3d2817; +} + +.detailItem span { + font-size: 1rem; + color: #5a4a3a; +} + +.imagesSection { + margin-top: 2.5rem; + padding-top: 2rem; + border-top: 2px solid #3d2817; +} + +.imagesSectionTitle { + margin: 0 0 1.5rem 0; + font-size: 1.5rem; + font-weight: 700; + color: #3d2817; + text-align: center; +} + +.imagesGrid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1rem; +} + +.imageCard { + background: #fff; + border-radius: 1rem; + overflow: hidden; + box-shadow: 0 2px 8px rgba(61, 40, 23, 0.1); + cursor: pointer; + transition: + transform 0.2s, + box-shadow 0.2s; + position: relative; + border: 3px solid #3d2817; + height: 180px; + display: block; + text-decoration: none; +} + +.imageCard:hover { + transform: translateY(-4px); + box-shadow: 0 6px 16px rgba(255, 107, 157, 0.3); +} + +.imageCard:nth-child(1) { + background-color: #fce8b8; +} + +.imageCard:nth-child(2) { + background-color: #a8d8d4; +} + +.imageCard:nth-child(3) { + background-color: #f5c9b4; +} + +.imageCard:nth-child(4) { + background-color: #f5a973; +} + +.gridImage { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + padding: 10px; + box-sizing: border-box; + border-radius: 1rem; +} + +.loading, +.error { + text-align: center; + padding: 3rem; + font-size: 1.125rem; + color: #3d2817; +} + +.error { + color: #dc3545; +} + +.noImages { + text-align: center; + color: #8b7355; + font-style: italic; + padding: 2rem; + font-size: 1.1rem; +} + +@media (max-width: 768px) { + .overlay { + padding: 1rem; + } + + .modal { + max-height: 95vh; + padding: 2rem 1.5rem; + } + + .breedName { + font-size: 2rem; + } + + .detailsGrid { + grid-template-columns: 1fr; + padding: 1rem; + } + + .imagesGrid { + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; + } + + .imageCard { + height: 150px; + } +} + +@media (max-width: 480px) { + .modal { + padding: 1.5rem 1rem; + } + + .breedName { + font-size: 1.5rem; + } + + .detailsGrid { + gap: 0.75rem; + } + + .imagesGrid { + grid-template-columns: 1fr; + } +} diff --git a/src/components/breedModal/BreedModal.tsx b/src/components/breedModal/BreedModal.tsx new file mode 100644 index 00000000..3bb8a355 --- /dev/null +++ b/src/components/breedModal/BreedModal.tsx @@ -0,0 +1,56 @@ +import { useEffect } from 'react'; + +import { Details, Grid, Wrapper } from './components'; +import useBreedModal from './useBreedModal'; + +import C from './constants'; +import styles from './BreedModal.module.css'; + +const BreedModal = () => { + const { + breed, + images, + isLoading, + error, + loadBreedImages, + handleBackdropClick, + handleClose, + } = useBreedModal(); + + useEffect(() => { + loadBreedImages(); + }, [loadBreedImages]); + + const renderContent = () => { + if (isLoading) { + return
{C.LOADING_TEXT}
; + } + + if (error || !breed) { + return
{error || C.ERROR_TEXT}
; + } + + return ( +
+
+ + {images.length > 0 && ( +
+

+ {C.EXAMPLE_IMAGES_TITLE} +

+ +
+ )} +
+ ); + }; + + return ( + + {renderContent()} + + ); +}; + +export default BreedModal; diff --git a/src/components/breedModal/components/Details.tsx b/src/components/breedModal/components/Details.tsx new file mode 100644 index 00000000..0e92c5a7 --- /dev/null +++ b/src/components/breedModal/components/Details.tsx @@ -0,0 +1,27 @@ +import U from '../utils'; +import styles from '../BreedModal.module.css'; +import type { Breed } from '../../../types'; + +type DetailsProps = { + breed: Breed; +}; + +const Details = ({ breed }: DetailsProps) => ( +
+
+

{breed.name}

+

{breed.origin}

+
+

{breed.description}

+
+ {U.detailsData(breed).map(([label, value]) => ( +
+ {label} + {value} +
+ ))} +
+
+); + +export default Details; diff --git a/src/components/breedModal/components/Grid.tsx b/src/components/breedModal/components/Grid.tsx new file mode 100644 index 00000000..d0ca8450 --- /dev/null +++ b/src/components/breedModal/components/Grid.tsx @@ -0,0 +1,31 @@ +import { Link } from 'react-router-dom'; + +import C from '../constants'; +import styles from '../BreedModal.module.css'; +import type { Cat } from '../../../types'; + +type BreedImageGridProps = { + images: Cat[]; +}; + +const BreedImageGrid = ({ images }: BreedImageGridProps) => { + return ( +
+ {images.slice(0, C.MAX_IMAGES_TO_DISPLAY).map((image) => ( + + {`Cat + + ))} +
+ ); +}; + +export default BreedImageGrid; diff --git a/src/components/breedModal/components/Wrapper.tsx b/src/components/breedModal/components/Wrapper.tsx new file mode 100644 index 00000000..58886457 --- /dev/null +++ b/src/components/breedModal/components/Wrapper.tsx @@ -0,0 +1,20 @@ +import styles from '../BreedModal.module.css'; + +type WrapperProps = { + onBackdropClick: (e: React.MouseEvent) => void; + onClose: () => void; + children: React.ReactNode; +}; + +const Wrapper = ({ onBackdropClick, onClose, children }: WrapperProps) => ( +
+
+ + {children} +
+
+); + +export default Wrapper; diff --git a/src/components/breedModal/components/index.ts b/src/components/breedModal/components/index.ts new file mode 100644 index 00000000..3e6bf31d --- /dev/null +++ b/src/components/breedModal/components/index.ts @@ -0,0 +1,3 @@ +export { default as Details } from './Details'; +export { default as Grid } from './Grid'; +export { default as Wrapper } from './Wrapper'; diff --git a/src/components/breedModal/constants.ts b/src/components/breedModal/constants.ts new file mode 100644 index 00000000..27080afb --- /dev/null +++ b/src/components/breedModal/constants.ts @@ -0,0 +1,37 @@ +const DETAIL_LABELS_AFFECTION_LEVEL = 'Affection Level'; +const DETAIL_LABELS_CHILD_FRIENDLY = 'Child Friendly'; +const DETAIL_LABELS_DOG_FRIENDLY = 'Dog Friendly'; +const DETAIL_LABELS_LIFE_SPAN = 'Life Span'; +const DETAIL_LABELS_TEMPERAMENT = 'Temperament'; +const DETAIL_LABELS_WEIGHT = 'Weight'; +const ERROR_TEXT = 'Failed to load breed images'; +const EXAMPLE_IMAGES_TITLE = 'Breed Cats'; +const LOADING_TEXT = 'Loading breed images...'; +const MAX_IMAGES_TO_DISPLAY = 4; +const MAX_RATING = 5; +const NO_IMAGES_TEXT = 'No images found for this breed'; +const STAR_EMPTY = 'โ˜†'; +const STAR_FILLED = 'โ˜…'; +const WEIGHT_UNITS_KG = 'kg'; +const WEIGHT_UNITS_LBS = 'lbs'; +const YEARS_SUFFIX = 'years'; + +export default { + DETAIL_LABELS_AFFECTION_LEVEL, + DETAIL_LABELS_CHILD_FRIENDLY, + DETAIL_LABELS_DOG_FRIENDLY, + DETAIL_LABELS_LIFE_SPAN, + DETAIL_LABELS_TEMPERAMENT, + DETAIL_LABELS_WEIGHT, + ERROR_TEXT, + EXAMPLE_IMAGES_TITLE, + LOADING_TEXT, + MAX_IMAGES_TO_DISPLAY, + MAX_RATING, + NO_IMAGES_TEXT, + STAR_EMPTY, + STAR_FILLED, + WEIGHT_UNITS_KG, + WEIGHT_UNITS_LBS, + YEARS_SUFFIX, +}; diff --git a/src/components/breedModal/index.ts b/src/components/breedModal/index.ts new file mode 100644 index 00000000..eddc094e --- /dev/null +++ b/src/components/breedModal/index.ts @@ -0,0 +1 @@ +export { default } from './BreedModal'; diff --git a/src/components/breedModal/useBreedModal.ts b/src/components/breedModal/useBreedModal.ts new file mode 100644 index 00000000..fee8054c --- /dev/null +++ b/src/components/breedModal/useBreedModal.ts @@ -0,0 +1,74 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; + +import { fetchImagesByBreed } from '../../api'; +import { analyticsActions } from '../../store/analyticsSlice'; +import { + selectBreeds, + useCatsDispatch, + useCatsSelector, +} from '../../store/hooks'; + +import C from './constants'; +import type { Breed, Cat } from '../../types'; + +const useBreedModal = () => { + const [images, setImages] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const navigate = useNavigate(); + const { breedId } = useParams<{ breedId: string }>(); + const dispatch = useCatsDispatch(); + + const breeds = useCatsSelector(selectBreeds); + const breed: Breed | null = breedId ? breeds[breedId] : null; + const breedName: string = breed?.name || ''; + + const loadBreedImages = useCallback(() => { + if (!breedId) { + return; + } + setIsLoading(true); + setError(null); + + fetchImagesByBreed(breedId, C.MAX_IMAGES_TO_DISPLAY) + .then((data: Cat[]) => { + setImages(data); + }) + .catch(() => { + setError(C.ERROR_TEXT); + }) + .finally(() => { + setIsLoading(false); + }); + }, [breedId]); + + const handleClose = (): void => { + navigate('/breeds'); + }; + + const handleBackdropClick = (event: React.MouseEvent): void => { + if (event.target === event.currentTarget) { + handleClose(); + } + }; + + useEffect(() => { + if (breedName) { + dispatch(analyticsActions.incrementBreedView({ breedName })); + } + }, [breedName, dispatch]); + + return { + breed, + images, + isLoading, + error, + loadBreedImages, + handleBackdropClick, + handleClose, + }; +}; + +export default useBreedModal; diff --git a/src/components/breedModal/utils.ts b/src/components/breedModal/utils.ts new file mode 100644 index 00000000..6fdb93ed --- /dev/null +++ b/src/components/breedModal/utils.ts @@ -0,0 +1,37 @@ +import C from './constants'; +import type { Breed } from '../../types'; + +const createStarRating = (rating: number): string => { + return ( + C.STAR_FILLED.repeat(rating) + C.STAR_EMPTY.repeat(C.MAX_RATING - rating) + ); +}; + +const formatWeight = (breed: Breed): string => { + return `${breed.weight.metric} ${C.WEIGHT_UNITS_KG} (${breed.weight.imperial} ${C.WEIGHT_UNITS_LBS})`; +}; + +const formatLifeSpan = (lifeSpan: string): string => { + return `${lifeSpan} ${C.YEARS_SUFFIX}`; +}; + +const getImageAltText = (imageId: string): string => { + return `Cat image ${imageId}`; +}; + +const detailsData = (breed: Breed): Array<[string, string]> => [ + [C.DETAIL_LABELS_TEMPERAMENT, breed.temperament], + [C.DETAIL_LABELS_LIFE_SPAN, formatLifeSpan(breed.life_span)], + [C.DETAIL_LABELS_WEIGHT, formatWeight(breed)], + [C.DETAIL_LABELS_AFFECTION_LEVEL, createStarRating(breed.affection_level)], + [C.DETAIL_LABELS_CHILD_FRIENDLY, createStarRating(breed.child_friendly)], + [C.DETAIL_LABELS_DOG_FRIENDLY, createStarRating(breed.dog_friendly)], +]; + +export default { + createStarRating, + formatWeight, + formatLifeSpan, + getImageAltText, + detailsData, +}; diff --git a/src/components/card/Card.module.css b/src/components/card/Card.module.css new file mode 100644 index 00000000..d3d8b01a --- /dev/null +++ b/src/components/card/Card.module.css @@ -0,0 +1,102 @@ +.card { + background: #fff; + border-radius: 1rem; + overflow: hidden; + box-shadow: 0 2px 8px rgba(61, 40, 23, 0.1); + cursor: pointer; + transition: + transform 0.2s, + box-shadow 0.2s; + position: relative; + border: 3px solid #3d2817; +} + +.card:hover { + transform: translateY(-8px) rotate(1deg); + box-shadow: 0 8px 24px rgba(255, 107, 157, 0.3); +} + +.card:nth-child(4n + 1) { + background-color: #fce8b8; +} + +.card:nth-child(4n + 2) { + background-color: #a8d8d4; +} + +.card:nth-child(4n + 3) { + background-color: #f5c9b4; +} + +.card:nth-child(4n + 4) { + background-color: #f5a973; +} + +.imageContainer { + position: relative; + width: 100%; + height: 220px; + overflow: hidden; + background-color: inherit; + padding: 10px; + box-sizing: border-box; +} + +.image { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + border-radius: 0.5rem; + transition: opacity 0.3s ease; +} + +.skeleton { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 0.5rem; + background: linear-gradient( + 90deg, + rgba(61, 40, 23, 0.05) 25%, + rgba(61, 40, 23, 0.1) 50%, + rgba(61, 40, 23, 0.05) 75% + ); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; +} + +@keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +.favouriteButtonWrapper { + position: absolute; + top: 0.75rem; + right: 0.75rem; + z-index: 10; +} + +.breedName { + position: absolute; + bottom: 1rem; + left: 3rem; + right: 3rem; + font-size: 1.25rem; + font-weight: 800; + color: #3d2817; + background: rgba(255, 255, 255, 0.9); + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + margin: 0; + text-align: center; + backdrop-filter: blur(4px); + z-index: 5; +} diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx new file mode 100644 index 00000000..ace62c21 --- /dev/null +++ b/src/components/card/Card.tsx @@ -0,0 +1,48 @@ +import CardImage from './CardImage'; +import CardSkeleton from './CardSkeleton'; +import useCard from './useCard'; +import FavouriteButton from '../favouriteButton'; + +import U from './utils'; +import styles from './Card.module.css'; +import type { Cat } from '../../types'; + +type CardProps = { + cat: Cat; + isFavourite: boolean; + isBreed: boolean; +}; + +const Card = ({ cat, isFavourite, isBreed }: CardProps) => { + const { breedText, isLoaded, handleCardClick, handleImageLoaded } = useCard({ + cat, + isBreed, + }); + + return ( +
+
+ {!isLoaded && } + + {!isBreed && ( +
+ +
+ )} + {isBreed && breedText && ( +
{breedText}
+ )} +
+
+ ); +}; + +export default Card; diff --git a/src/components/card/CardImage.tsx b/src/components/card/CardImage.tsx new file mode 100644 index 00000000..4731ea3f --- /dev/null +++ b/src/components/card/CardImage.tsx @@ -0,0 +1,22 @@ +import styles from './Card.module.css'; +import type { Cat } from '../../types'; + +type CardImageProps = { + cat: Cat; + imagePosition: string; + onLoad: () => void; +}; + +const CardImage = ({ cat, imagePosition, onLoad }: CardImageProps) => ( + {cat.id} +); + +export default CardImage; diff --git a/src/components/card/CardSkeleton.tsx b/src/components/card/CardSkeleton.tsx new file mode 100644 index 00000000..40fbf0dc --- /dev/null +++ b/src/components/card/CardSkeleton.tsx @@ -0,0 +1,5 @@ +import styles from './Card.module.css'; + +const CardSkeleton = () =>
; + +export default CardSkeleton; diff --git a/src/components/card/constants.ts b/src/components/card/constants.ts new file mode 100644 index 00000000..391bef1e --- /dev/null +++ b/src/components/card/constants.ts @@ -0,0 +1,7 @@ +const HIGH_IMAGE_RATIO_THRESHOLD = 1.2; +const MEDIUM_IMAGE_RATIO_THRESHOLD = 1.1; + +export default { + HIGH_IMAGE_RATIO_THRESHOLD, + MEDIUM_IMAGE_RATIO_THRESHOLD, +}; diff --git a/src/components/card/index.ts b/src/components/card/index.ts new file mode 100644 index 00000000..c68311df --- /dev/null +++ b/src/components/card/index.ts @@ -0,0 +1 @@ +export { default } from './Card'; diff --git a/src/components/card/useCard.ts b/src/components/card/useCard.ts new file mode 100644 index 00000000..5d3ca00c --- /dev/null +++ b/src/components/card/useCard.ts @@ -0,0 +1,44 @@ +import { useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import U from './utils'; +import type { Cat } from '../../types'; + +type UseCardProps = { + cat: Cat; + isBreed: boolean; +}; + +const useCard = ({ cat, isBreed }: UseCardProps) => { + const [isLoaded, setIsLoaded] = useState(false); + const navigate = useNavigate(); + const location = useLocation(); + + const hasBreeds: boolean = !!cat.breeds?.length; + const breedId: string = cat.breeds?.[0]?.id ?? ''; + const breedText: string = hasBreeds ? U.formatBreedNames(cat.breeds!) : ''; + + const handleCardClick = () => { + if (isBreed && breedId) { + navigate(`/breeds/${breedId}`); + } else { + const basePath = location.pathname.startsWith('/favourites') + ? '/favourites' + : ''; + navigate(`${basePath}/cat/${cat.id}`); + } + }; + + const handleImageLoaded = () => { + setIsLoaded(true); + }; + + return { + breedText, + isLoaded, + handleCardClick, + handleImageLoaded, + }; +}; + +export default useCard; diff --git a/src/components/card/utils.ts b/src/components/card/utils.ts new file mode 100644 index 00000000..231ca780 --- /dev/null +++ b/src/components/card/utils.ts @@ -0,0 +1,18 @@ +import C from './constants'; +import type { Breed,Cat } from '../../types'; + +const getImagePosition = (cat: Cat): string => { + const imageRatio = cat.height / cat.width; + if (imageRatio > C.HIGH_IMAGE_RATIO_THRESHOLD) return 'center 15%'; + if (imageRatio > C.MEDIUM_IMAGE_RATIO_THRESHOLD) return 'center 25%'; + return 'center'; +}; + +const formatBreedNames = (breeds: Breed[]): string => { + return breeds.map((breed) => breed.name).join(' - '); +}; + +export default { + formatBreedNames, + getImagePosition, +}; diff --git a/src/components/errorMessage/ErrorMessage.module.css b/src/components/errorMessage/ErrorMessage.module.css new file mode 100644 index 00000000..cafb8621 --- /dev/null +++ b/src/components/errorMessage/ErrorMessage.module.css @@ -0,0 +1,7 @@ +.errorMessage { + color: #dc3545; + text-align: center; + margin: 1.5rem 0; + font-size: 1rem; + font-weight: 500; +} diff --git a/src/components/errorMessage/ErrorMessage.tsx b/src/components/errorMessage/ErrorMessage.tsx new file mode 100644 index 00000000..41f9fa79 --- /dev/null +++ b/src/components/errorMessage/ErrorMessage.tsx @@ -0,0 +1,11 @@ +import styles from './ErrorMessage.module.css'; + +type ErrorMessageProps = { + message: string; +}; + +const ErrorMessage = ({ message }: ErrorMessageProps) => { + return

{message}

; +}; + +export default ErrorMessage; diff --git a/src/components/errorMessage/index.ts b/src/components/errorMessage/index.ts new file mode 100644 index 00000000..43c8c9cc --- /dev/null +++ b/src/components/errorMessage/index.ts @@ -0,0 +1 @@ +export { default } from './ErrorMessage'; diff --git a/src/components/favouriteButton/FavouriteButton.module.css b/src/components/favouriteButton/FavouriteButton.module.css new file mode 100644 index 00000000..1023bf15 --- /dev/null +++ b/src/components/favouriteButton/FavouriteButton.module.css @@ -0,0 +1,92 @@ +.compactButton, +.expandedButton { + display: flex; + align-items: center; + cursor: pointer; +} + +.compactButton { + background: none; + border: none; + box-shadow: none; + padding: 0.7rem; + justify-content: center; + transition: transform 0.2s; + color: #3d2817; + position: relative; +} +.compactButton:focus { + outline: none; + box-shadow: none; +} +.compactButton::after { + content: attr(data-tooltip); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background: #3d2817; + color: #fff; + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + font-size: 0.75rem; + font-weight: 600; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; + z-index: 100; + margin-bottom: 0.5rem; + display: none; +} +.compactButton[data-tooltip]:not([data-tooltip=''])::after { + display: block; +} +.compactButton:hover { + transform: scale(1.15); +} +.compactButton:hover::after { + opacity: 1; +} +.compactButton.active:hover { + animation: heartBeat 0.3s ease-in-out; +} + +@keyframes heartBeat { + 0%, + 100% { + transform: scale(1); + } + 50% { + transform: scale(1.2); + } +} + +.expandedButton { + gap: 0.5rem; + padding: 0.75rem 1.5rem; + font-size: 1rem; + background: #fff; + color: #3d2817; + border: 2px solid #3d2817; + border-radius: 2rem; + transition: all 0.2s; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; +} +.expandedButton:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(61, 40, 23, 0.2); + background: #fce8b8; +} +.expandedButton.active { + background: #ff6b9d; + color: #fff; + border-color: #ff6b9d; +} +.expandedButton.active:hover { + background: #ff4d88; + border-color: #ff4d88; +} diff --git a/src/components/favouriteButton/FavouriteButton.tsx b/src/components/favouriteButton/FavouriteButton.tsx new file mode 100644 index 00000000..da57e643 --- /dev/null +++ b/src/components/favouriteButton/FavouriteButton.tsx @@ -0,0 +1,38 @@ +import FavouriteButtonImage from './FavouriteButtonImage'; +import useFavouriteButton from './useFavouriteButton'; + +import type { Cat } from '../../types'; + +type FavouriteButtonProps = { + cat: Cat; + isFavourite: boolean; + variant: 'compact' | 'expanded'; +}; + +const FavouriteButton = ({ + cat, + isFavourite, + variant, +}: FavouriteButtonProps) => { + const { + text, + buttonClass, + fillColor, + strokeColor, + showText, + handleFavouriteToggle, + } = useFavouriteButton({ cat, isFavourite, variant }); + + return ( + + ); +}; + +export default FavouriteButton; diff --git a/src/components/favouriteButton/FavouriteButtonImage.tsx b/src/components/favouriteButton/FavouriteButtonImage.tsx new file mode 100644 index 00000000..d6aa5d98 --- /dev/null +++ b/src/components/favouriteButton/FavouriteButtonImage.tsx @@ -0,0 +1,27 @@ +type FavouriteButtonImageProps = { + fillColor: string; + strokeColor: string; +}; + +const FavouriteButtonImage = ({ + fillColor, + strokeColor, +}: FavouriteButtonImageProps) => ( + +); + +export default FavouriteButtonImage; diff --git a/src/components/favouriteButton/constants.ts b/src/components/favouriteButton/constants.ts new file mode 100644 index 00000000..647b5e8b --- /dev/null +++ b/src/components/favouriteButton/constants.ts @@ -0,0 +1,16 @@ +const ADD_TEXT = 'Add to favorites'; +const HEART_FILL_COMPACT = '#f5a973'; +const HEART_FILL_EXPANDED = '#fff'; +const HEART_STROKE_COMPACT = '#f5a973'; +const HEART_STROKE_EXPANDED = '#ff6b9d'; + +const REMOVE_TEXT = 'Remove from favorites'; + +export default { + ADD_TEXT, + HEART_FILL_COMPACT, + HEART_FILL_EXPANDED, + HEART_STROKE_COMPACT, + HEART_STROKE_EXPANDED, + REMOVE_TEXT, +}; diff --git a/src/components/favouriteButton/index.tsx b/src/components/favouriteButton/index.tsx new file mode 100644 index 00000000..8b45f738 --- /dev/null +++ b/src/components/favouriteButton/index.tsx @@ -0,0 +1 @@ +export { default } from './FavouriteButton'; diff --git a/src/components/favouriteButton/useFavouriteButton.ts b/src/components/favouriteButton/useFavouriteButton.ts new file mode 100644 index 00000000..37852cf1 --- /dev/null +++ b/src/components/favouriteButton/useFavouriteButton.ts @@ -0,0 +1,40 @@ +import { favouritesActions } from '../../store/favouritesSlice'; +import { useCatsDispatch } from '../../store/hooks'; + +import U from './utils'; +import type { Cat } from '../../types'; + +type UseFavouriteButtonProps = { + cat: Cat; + isFavourite: boolean; + variant: 'compact' | 'expanded'; +}; + +const useFavouriteButton = ({ + cat, + isFavourite, + variant, +}: UseFavouriteButtonProps) => { + const dispatch = useCatsDispatch(); + + const handleFavouriteToggle = (event: React.MouseEvent) => { + event.stopPropagation(); + dispatch(favouritesActions.toggleFavourite(cat)); + }; + const isCompact = variant === 'compact'; + const text = U.getButtonText(isFavourite); + const fillColor = U.getFillColor(isCompact, isFavourite); + const strokeColor = U.getStrokeColor(isCompact); + const buttonClass = U.getButtonClassName(isCompact, isFavourite); + + return { + text, + buttonClass, + fillColor, + strokeColor, + showText: !isCompact, + handleFavouriteToggle, + }; +}; + +export default useFavouriteButton; diff --git a/src/components/favouriteButton/utils.ts b/src/components/favouriteButton/utils.ts new file mode 100644 index 00000000..8f5d9813 --- /dev/null +++ b/src/components/favouriteButton/utils.ts @@ -0,0 +1,32 @@ +import C from './constants'; +import styles from './FavouriteButton.module.css'; + +const getFillColor = (isCompact: boolean, isFavourite: boolean): string => { + if (!isFavourite) return 'none'; + return isCompact ? C.HEART_FILL_COMPACT : C.HEART_FILL_EXPANDED; +}; + +const getStrokeColor = (isCompact: boolean): string => { + return isCompact ? C.HEART_STROKE_COMPACT : C.HEART_STROKE_EXPANDED; +}; + +const getButtonClassName = ( + isCompact: boolean, + isFavourite: boolean +): string => { + const baseClass = isCompact ? styles.compactButton : styles.expandedButton; + return isFavourite ? `${baseClass} ${styles.active}` : baseClass; +}; + +const getButtonText = (isFavourite: boolean): string => { + return isFavourite ? C.REMOVE_TEXT : C.ADD_TEXT; +}; + +const U = { + getFillColor, + getStrokeColor, + getButtonClassName, + getButtonText, +}; + +export default U; diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 00000000..24f98502 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,6 @@ +export { default as BreedModal } from './breedModal'; +export { default as Card } from './card'; +export { default as ErrorMessage } from './errorMessage'; +export { default as LoadingSpinner } from './loadingSpinner'; +export { default as Modal } from './modal'; +export { default as Navigation } from './navigation'; diff --git a/src/components/loadingSpinner/LoadingSpinner.module.css b/src/components/loadingSpinner/LoadingSpinner.module.css new file mode 100644 index 00000000..da749d6d --- /dev/null +++ b/src/components/loadingSpinner/LoadingSpinner.module.css @@ -0,0 +1,32 @@ +.loadingContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 3rem 0; +} + +.spinner { + width: 3rem; + height: 3rem; + border: 4px solid #e5d5b8; + border-top: 4px solid #fcc76f; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.loadingMessage { + margin-top: 1rem; + color: #3d2817; + font-size: 1.5rem; + font-weight: 600; +} diff --git a/src/components/loadingSpinner/LoadingSpinner.tsx b/src/components/loadingSpinner/LoadingSpinner.tsx new file mode 100644 index 00000000..f09172db --- /dev/null +++ b/src/components/loadingSpinner/LoadingSpinner.tsx @@ -0,0 +1,13 @@ +import C from './constants'; +import styles from './LoadingSpinner.module.css'; + +const LoadingSpinner = () => { + return ( +
+
+

{C.LOADING_TEXT}

+
+ ); +}; + +export default LoadingSpinner; diff --git a/src/components/loadingSpinner/constants.ts b/src/components/loadingSpinner/constants.ts new file mode 100644 index 00000000..2d1d687e --- /dev/null +++ b/src/components/loadingSpinner/constants.ts @@ -0,0 +1,3 @@ +const LOADING_TEXT = 'Loading ...'; + +export default { LOADING_TEXT }; diff --git a/src/components/loadingSpinner/index.ts b/src/components/loadingSpinner/index.ts new file mode 100644 index 00000000..6513c5cb --- /dev/null +++ b/src/components/loadingSpinner/index.ts @@ -0,0 +1 @@ +export { default } from './LoadingSpinner'; diff --git a/src/components/modal/Modal.module.css b/src/components/modal/Modal.module.css new file mode 100644 index 00000000..0be6aad6 --- /dev/null +++ b/src/components/modal/Modal.module.css @@ -0,0 +1,181 @@ +.overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(61, 40, 23, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 2rem; + backdrop-filter: blur(4px); +} + +.modal { + background: #fff; + border-radius: 1rem; + max-width: 700px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + position: relative; + border: 3px solid #3d2817; + box-shadow: 0 8px 32px rgba(61, 40, 23, 0.3); + padding: 3rem 3rem 0 3rem; +} + +.closeButton { + position: absolute; + top: 1rem; + right: 1rem; + background: none; + border: none; + border-radius: 0; + width: auto; + height: auto; + font-size: 2rem; + cursor: pointer; + color: #3d2817; + font-weight: bold; + z-index: 10; + padding: 0; + line-height: 1; +} +.closeButton:hover { + color: #ff6b9d; + background: none; + transform: none; +} + +.imageContainer { + width: 100%; + max-height: 300px; + overflow: hidden; + background: #fff; + display: flex; + align-items: center; + justify-content: center; + margin: 0; + border-radius: 1rem 1rem 0 0; +} + +.image { + width: 100%; + height: auto; + max-height: 300px; + object-fit: contain; + display: block; +} + +.content { + padding: 2rem 3rem; + display: flex; + flex-direction: column; + align-items: center; +} + +.favouriteButtonContainer { + display: flex; + justify-content: center; + margin-bottom: 1rem; +} + +.loading, +.error { + text-align: center; + padding: 3rem; + font-size: 1.125rem; + color: #3d2817; +} + +.error { + color: #dc3545; +} + +.breedInfo { + text-align: center; +} + +.breedName { + margin: 0 0 1rem 0; + font-size: 2rem; + font-weight: 800; + color: #3d2817; +} + +.breedDescription { + margin: 0 0 1.5rem 0; + font-size: 1rem; + color: #5a4a3a; + line-height: 1.6; +} + +.breedDetails { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-bottom: 1.5rem; + text-align: left; + background: #fce8b8; + padding: 1.5rem; + border-radius: 0.5rem; +} + +.detailItem { + font-size: 0.9375rem; + color: #3d2817; + line-height: 1.5; +} + +.detailItem strong { + font-weight: 700; + margin-right: 0.5rem; +} + +.breedLink { + display: inline-block; + padding: 0.75rem 1.5rem; + background: #fcc76f; + color: #3d2817; + text-decoration: none; + border-radius: 2rem; + font-weight: 600; + transition: all 0.2s; + border: 2px solid #3d2817; +} + +.breedLink:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(61, 40, 23, 0.2); + background: #fbb94e; +} + +.noBreed { + text-align: center; + color: #8b7355; + font-style: italic; +} + +@media (max-width: 768px) { + .overlay { + padding: 1rem; + } + + .modal { + max-height: 95vh; + } + + .content { + padding: 1.5rem; + } + + .breedName { + font-size: 1.5rem; + } + + .breedDetails { + padding: 1rem; + } +} diff --git a/src/components/modal/Modal.tsx b/src/components/modal/Modal.tsx new file mode 100644 index 00000000..d8f4f0fa --- /dev/null +++ b/src/components/modal/Modal.tsx @@ -0,0 +1,56 @@ +import { Details, Image, Link, Wrapper } from './components'; +import useModal from './useModal'; +import FavouriteButton from '../favouriteButton'; + +import U from './utils'; +import C from './constants'; +import styles from './Modal.module.css'; + +const Modal = () => { + const { + image, + isLoading, + error, + isFavourite, + breedLinkPath, + handleClose, + handleBackdropClick, + } = useModal(); + + const renderContent = () => { + if (isLoading) { + return
{C.LOADING_TEXT}
; + } + + if (error || !image) { + return
{error || C.NOT_FOUND_TEXT}
; + } + + const breed = U.getBreedInfo(image); + + return ( + <> + +
+
+ +
+
+ +
+ + ); + }; + + return ( + + {renderContent()} + + ); +}; + +export default Modal; diff --git a/src/components/modal/components/Details.tsx b/src/components/modal/components/Details.tsx new file mode 100644 index 00000000..eb8fa672 --- /dev/null +++ b/src/components/modal/components/Details.tsx @@ -0,0 +1,35 @@ +import C from '../constants'; +import styles from '../Modal.module.css'; +import type { Breed } from '../../../types'; + +type DetailsProps = { + breed: Breed | null; +}; + +const Details = ({ breed }: DetailsProps) => { + if (!breed) { + return

{C.NO_BREED_TEXT}

; + } + + return ( +
+

{breed.name}

+

{breed.description}

+ +
+
+ {C.DETAIL_LABELS_ORIGIN}: {breed.origin} +
+
+ {C.DETAIL_LABELS_TEMPERAMENT}: {breed.temperament} +
+
+ {C.DETAIL_LABELS_LIFE_SPAN}: {breed.life_span}{' '} + {C.YEARS_SUFFIX} +
+
+
+ ); +}; + +export default Details; diff --git a/src/components/modal/components/Image.tsx b/src/components/modal/components/Image.tsx new file mode 100644 index 00000000..02e83939 --- /dev/null +++ b/src/components/modal/components/Image.tsx @@ -0,0 +1,14 @@ +import C from '../constants'; +import styles from '../Modal.module.css'; + +type ModalImageProps = { + imageUrl: string; +}; + +const ModalImage = ({ imageUrl }: ModalImageProps) => ( +
+ {C.CAT_IMAGE} +
+); + +export default ModalImage; diff --git a/src/components/modal/components/Link.tsx b/src/components/modal/components/Link.tsx new file mode 100644 index 00000000..6d767e04 --- /dev/null +++ b/src/components/modal/components/Link.tsx @@ -0,0 +1,23 @@ +import { Link as LinkToBreed } from 'react-router-dom'; + +import C from '../constants'; +import styles from '../Modal.module.css'; +import type { Breed } from '../../../types'; + +type LinkProps = { + breed: Breed | null; + breedLinkPath: string; +}; + +const Link = ({ breed, breedLinkPath }: LinkProps) => { + if (!breed) { + return; + } + return ( + + {C.VIEW_ALL_PREFIX} {breed.name} {C.VIEW_ALL_SUFFIX} + + ); +}; + +export default Link; diff --git a/src/components/modal/components/Wrapper.tsx b/src/components/modal/components/Wrapper.tsx new file mode 100644 index 00000000..d72e92a1 --- /dev/null +++ b/src/components/modal/components/Wrapper.tsx @@ -0,0 +1,20 @@ +import styles from '../Modal.module.css'; + +type WrapperProps = { + onBackdropClick: (e: React.MouseEvent) => void; + onClose: () => void; + children: React.ReactNode; +}; + +const Wrapper = ({ onBackdropClick, onClose, children }: WrapperProps) => ( +
+
+ + {children} +
+
+); + +export default Wrapper; diff --git a/src/components/modal/components/index.ts b/src/components/modal/components/index.ts new file mode 100644 index 00000000..f5944c0e --- /dev/null +++ b/src/components/modal/components/index.ts @@ -0,0 +1,4 @@ +export { default as Details } from './Details'; +export { default as Image } from './Image'; +export { default as Link } from './Link'; +export { default as Wrapper } from './Wrapper'; diff --git a/src/components/modal/constants.ts b/src/components/modal/constants.ts new file mode 100644 index 00000000..4b711d97 --- /dev/null +++ b/src/components/modal/constants.ts @@ -0,0 +1,25 @@ +const CAT_IMAGE = 'cat image'; +const ERROR_TEXT = 'Failed to load cat image'; +const LOADING_TEXT = 'Loading...'; +const NOT_FOUND_TEXT = 'Image not found'; +const NO_BREED_TEXT = 'No breed information available'; +const VIEW_ALL_PREFIX = 'View all'; +const VIEW_ALL_SUFFIX = 'cats โ†’'; +const DETAIL_LABELS_ORIGIN = 'Origin'; +const DETAIL_LABELS_TEMPERAMENT = 'Temperament'; +const DETAIL_LABELS_LIFE_SPAN = 'Life Span'; +const YEARS_SUFFIX = 'years'; + +export default { + CAT_IMAGE, + ERROR_TEXT, + LOADING_TEXT, + NOT_FOUND_TEXT, + NO_BREED_TEXT, + VIEW_ALL_PREFIX, + VIEW_ALL_SUFFIX, + DETAIL_LABELS_ORIGIN, + DETAIL_LABELS_TEMPERAMENT, + DETAIL_LABELS_LIFE_SPAN, + YEARS_SUFFIX, +}; diff --git a/src/components/modal/index.ts b/src/components/modal/index.ts new file mode 100644 index 00000000..0690fecf --- /dev/null +++ b/src/components/modal/index.ts @@ -0,0 +1 @@ +export { default } from './Modal'; diff --git a/src/components/modal/useModal.ts b/src/components/modal/useModal.ts new file mode 100644 index 00000000..df28dbe5 --- /dev/null +++ b/src/components/modal/useModal.ts @@ -0,0 +1,80 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; + +import { fetchCatImageById } from '../../api'; +import { + selectFavourites, + selectGallery, + useCatsSelector, +} from '../../store/hooks'; + +import U from './utils'; +import C from './constants'; +import type { Cat } from '../../types'; + +export const useModal = () => { + const imageId = useParams<{ imageId: string }>().imageId || ''; + const navigate = useNavigate(); + const location = useLocation(); + + const cachedCats: Record = useCatsSelector(selectGallery); + const favouriteCats: Record = useCatsSelector(selectFavourites); + const cachedCat: Cat = cachedCats?.[imageId]; + const isFavourite: boolean = !!favouriteCats[imageId]; + + const [image, setImage] = useState(cachedCat ?? null); + const [isLoading, setIsLoading] = useState(!cachedCat); + const [error, setError] = useState(null); + + const loadImage = useCallback(() => { + if (!imageId || cachedCat) return; + + fetchCatImageById(imageId) + .then((data: Cat) => { + setImage(data); + setError(null); + setIsLoading(false); + }) + .catch(() => { + setError(C.ERROR_TEXT); + setIsLoading(false); + }); + }, [imageId, cachedCat]); + + const getBreedLinkPath = useCallback((): string => { + const breed = U.getBreedInfo(image); + if (!breed) return ''; + + return U.calculateBreedLinkPath(breed.id, location.pathname); + }, [image, location.pathname]); + + const handleClose = useCallback(() => { + const parentPath = U.getParentPath(location.pathname); + navigate(parentPath); + }, [location.pathname, navigate]); + + const handleBackdropClick = useCallback( + (event: React.MouseEvent) => { + if (event.target === event.currentTarget) { + handleClose(); + } + }, + [handleClose] + ); + + useEffect(() => { + loadImage(); + }, [loadImage]); + + return { + image, + isLoading, + error, + isFavourite, + breedLinkPath: getBreedLinkPath(), + handleBackdropClick, + handleClose, + }; +}; + +export default useModal; diff --git a/src/components/modal/utils.ts b/src/components/modal/utils.ts new file mode 100644 index 00000000..7d1a06d9 --- /dev/null +++ b/src/components/modal/utils.ts @@ -0,0 +1,30 @@ +import type { Cat } from '../../types'; + +const getBreedInfo = (image: Cat | null) => { + if (!image?.breeds?.length) return null; + return image.breeds[0]; +}; + +const getBreedLinkPath = (breedId: string): string => { + return `/breeds/${breedId}`; +}; + +const calculateBreedLinkPath = ( + breedId: string, + currentPath: string +): string => { + const isInBreedModal = currentPath.startsWith(`/breeds/${breedId}`); + return isInBreedModal ? `/breeds/${breedId}` : getBreedLinkPath(breedId); +}; + +const getParentPath = (pathname: string): string => { + const pathParts = pathname.split('/cat/')[0]; + return pathParts || '/'; +}; + +export default { + getBreedInfo, + getBreedLinkPath, + calculateBreedLinkPath, + getParentPath, +}; diff --git a/src/components/navigation/Navigation.module.css b/src/components/navigation/Navigation.module.css new file mode 100644 index 00000000..d1c59f02 --- /dev/null +++ b/src/components/navigation/Navigation.module.css @@ -0,0 +1,98 @@ +.nav { + background: #f4d4d0; + box-shadow: 0 2px 8px rgba(61, 40, 23, 0.1); + position: sticky; + top: 0; + z-index: 100; + padding: 1rem 2rem; + width: 100vw; + margin-left: calc(-50vw + 50%); + box-sizing: border-box; +} + +.container { + max-width: 1200px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; +} + +.logo { + display: flex; + align-items: center; + gap: 0.75rem; + text-decoration: none; + transition: transform 0.2s; +} + +.logo:hover { + transform: scale(1.05); +} + +.logoIcon { + width: 50px; + height: auto; + display: block; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2)); +} + +.logoText { + font-size: 1.5rem; + font-weight: 700; + color: #3d2817; + text-shadow: none; +} + +.links { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.link { + padding: 0.75rem 1.5rem; + color: #3d2817; + text-decoration: none; + font-weight: 500; + border-radius: 1.5rem; + transition: all 0.2s; + position: relative; +} + +.link:hover { + background: rgba(234, 144, 118, 0.2); + color: #3d2817; +} + +.linkActive { + background: #ea9076; + color: white; + font-weight: 600; +} + +.linkActive::after { + display: none; +} + +@media (max-width: 768px) { + .container { + padding: 1rem; + flex-direction: column; + gap: 1rem; + } + + .links { + width: 100%; + justify-content: center; + } + + .link { + padding: 0.5rem 1rem; + font-size: 0.875rem; + } + + .logoText { + font-size: 1.25rem; + } +} diff --git a/src/components/navigation/Navigation.tsx b/src/components/navigation/Navigation.tsx new file mode 100644 index 00000000..ca6bc079 --- /dev/null +++ b/src/components/navigation/Navigation.tsx @@ -0,0 +1,59 @@ +import { Link } from 'react-router-dom'; + +import useNavigation from './useNavigation'; + +import C from './constants'; +import styles from './Navigation.module.css'; + +const Navigation = () => { + const { + isGalleryActive, + isBreedsActive, + isFavouritesActive, + isDemographicsActive, + } = useNavigation(); + + return ( + + ); +}; + +export default Navigation; diff --git a/src/components/navigation/constants.ts b/src/components/navigation/constants.ts new file mode 100644 index 00000000..e47cb16f --- /dev/null +++ b/src/components/navigation/constants.ts @@ -0,0 +1,25 @@ +const BREEDS = 'Breeds'; +const BREEDS_PATH = '/breeds'; +const DEFAULT_PATH = '/'; +const GALLERY = 'Gallery'; +const IMAGE_SOURCE = '/cat.png'; +const IMAGE_ALT = 'Cat logo'; +const FAVOURITES = 'Favourites'; +const FAVOURITES_PATH = '/favourites'; +const ANALYTICS = 'Analytics'; +const ANALYTICS_PATH = '/analytics'; +const TITLE = 'Cat Lover'; + +export default { + BREEDS, + BREEDS_PATH, + DEFAULT_PATH, + GALLERY, + IMAGE_SOURCE, + IMAGE_ALT, + FAVOURITES, + FAVOURITES_PATH, + ANALYTICS, + ANALYTICS_PATH, + TITLE, +}; diff --git a/src/components/navigation/index.ts b/src/components/navigation/index.ts new file mode 100644 index 00000000..7bc6dc0e --- /dev/null +++ b/src/components/navigation/index.ts @@ -0,0 +1 @@ +export { default } from './Navigation'; diff --git a/src/components/navigation/useNavigation.ts b/src/components/navigation/useNavigation.ts new file mode 100644 index 00000000..978c8b8e --- /dev/null +++ b/src/components/navigation/useNavigation.ts @@ -0,0 +1,38 @@ +import { useLocation } from 'react-router-dom'; + +import C from './constants'; + +type UseNavigationState = { + isGalleryActive: boolean; + isBreedsActive: boolean; + isFavouritesActive: boolean; + isDemographicsActive: boolean; +}; + +const useNavigation = (): UseNavigationState => { + const location = useLocation(); + + const isActive = (path: string) => { + if (path === C.DEFAULT_PATH && location.pathname === C.DEFAULT_PATH) + return true; + if (path !== C.DEFAULT_PATH && location.pathname.startsWith(path)) + return true; + return false; + }; + + const isGalleryActive = + isActive(C.DEFAULT_PATH) || location.pathname.startsWith('/cat/'); + + const isBreedsActive = isActive(C.BREEDS_PATH); + const isFavouritesActive = isActive(C.FAVOURITES_PATH); + const isDemographicsActive = isActive(C.ANALYTICS_PATH); + + return { + isGalleryActive, + isBreedsActive, + isFavouritesActive, + isDemographicsActive, + }; +}; + +export default useNavigation; diff --git a/src/hooks/constants.ts b/src/hooks/constants.ts new file mode 100644 index 00000000..004f8f02 --- /dev/null +++ b/src/hooks/constants.ts @@ -0,0 +1,28 @@ +const ANALYTICS_LOAD_ERROR = 'Failed to load analytics from localStorage'; +const ANALYTICS_SAVE_ERROR = 'Failed to save analytics to localStorage'; +const BREEDS_LOAD_ERROR = 'Failed to load breeds from localStorage'; +const BREEDS_SAVE_ERROR = 'Failed to save breeds to localStorage'; +const FAVOURITES_INVALID_FORMAT = + 'Invalid favourites data format, resetting to empty object'; +const FAVOURITES_LOAD_ERROR = 'Failed to load favourites from localStorage'; +const FAVOURITES_SAVE_ERROR = 'Failed to save favourites to localStorage'; + +const STORAGE_KEY_ANALYTICS = 'cat-lover-analytics'; +const STORAGE_KEY_BREEDS = 'cat-lover-breeds'; +const STORAGE_KEY_FAVOURITES = 'cat_app_favourites'; + +const ONE_DAY_MS = 1000 * 60 * 60 * 24; + +export default { + ANALYTICS_LOAD_ERROR, + ANALYTICS_SAVE_ERROR, + BREEDS_LOAD_ERROR, + BREEDS_SAVE_ERROR, + FAVOURITES_INVALID_FORMAT, + FAVOURITES_LOAD_ERROR, + FAVOURITES_SAVE_ERROR, + ONE_DAY_MS, + STORAGE_KEY_ANALYTICS, + STORAGE_KEY_BREEDS, + STORAGE_KEY_FAVOURITES, +}; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 00000000..a7c15272 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,3 @@ +export { default as useAnalyticsPersistence } from './useAnalyticsPersistence'; +export { default as useBreedsPersistence } from './useBreedsPersistence'; +export { default as useFavouritesPersistence } from './useFavouritesPersistence'; diff --git a/src/hooks/useAnalyticsPersistence.ts b/src/hooks/useAnalyticsPersistence.ts new file mode 100644 index 00000000..a613ea9f --- /dev/null +++ b/src/hooks/useAnalyticsPersistence.ts @@ -0,0 +1,45 @@ +import { useEffect } from 'react'; + +import { analyticsActions } from '../store/analyticsSlice'; +import { + selectAnalytics, + useCatsDispatch, + useCatsSelector, +} from '../store/hooks'; + +import C from './constants'; + +const useAnalyticsPersistence = () => { + const dispatch = useCatsDispatch(); + const analytics = useCatsSelector(selectAnalytics); + + useEffect(() => { + const savedData = localStorage.getItem(C.STORAGE_KEY_ANALYTICS); + if (savedData) { + Promise.resolve() + .then(() => JSON.parse(savedData)) + .then((parsed) => { + const validatedData = { + galleryCats: parsed.galleryCats || 0, + breedsViewed: parsed.breedsViewed || {}, + }; + dispatch(analyticsActions.loadAnalytics(validatedData)); + }) + .catch((error) => { + console.error(C.ANALYTICS_LOAD_ERROR, error); + }); + } + }, [dispatch]); + + useEffect(() => { + Promise.resolve() + .then(() => + localStorage.setItem(C.STORAGE_KEY_ANALYTICS, JSON.stringify(analytics)) + ) + .catch((error) => { + console.error(C.ANALYTICS_SAVE_ERROR, error); + }); + }, [analytics]); +}; + +export default useAnalyticsPersistence; diff --git a/src/hooks/useBreedsPersistence.ts b/src/hooks/useBreedsPersistence.ts new file mode 100644 index 00000000..d3ab1dbe --- /dev/null +++ b/src/hooks/useBreedsPersistence.ts @@ -0,0 +1,50 @@ +import { useEffect } from 'react'; + +import type { BreedsStateWithMeta } from '../store/breedsSlice'; +import { breedsActions } from '../store/breedsSlice'; +import { + selectBreedsWithMeta, + useCatsDispatch, + useCatsSelector, +} from '../store/hooks'; + +import C from './constants'; + +const useBreedsPersistence = () => { + const dispatch = useCatsDispatch(); + const breeds = useCatsSelector(selectBreedsWithMeta); + + useEffect(() => { + const savedData = localStorage.getItem(C.STORAGE_KEY_BREEDS); + if (savedData) { + Promise.resolve() + .then(() => JSON.parse(savedData) as BreedsStateWithMeta) + .then((parsed) => { + const now = Date.now(); + const isFresh = + parsed.lastFetched && now - parsed.lastFetched < C.ONE_DAY_MS; + + if (isFresh && Object.keys(parsed.data).length > 0) { + dispatch(breedsActions.loadBreeds(parsed)); + } + }) + .catch((error) => { + console.error(C.BREEDS_LOAD_ERROR, error); + }); + } + }, [dispatch]); + + useEffect(() => { + if (Object.keys(breeds.data).length > 0) { + Promise.resolve() + .then(() => + localStorage.setItem(C.STORAGE_KEY_BREEDS, JSON.stringify(breeds)) + ) + .catch((error) => { + console.error(C.BREEDS_SAVE_ERROR, error); + }); + } + }, [breeds]); +}; + +export default useBreedsPersistence; diff --git a/src/hooks/useFavouritesPersistence.ts b/src/hooks/useFavouritesPersistence.ts new file mode 100644 index 00000000..5802729b --- /dev/null +++ b/src/hooks/useFavouritesPersistence.ts @@ -0,0 +1,49 @@ +import { useEffect } from 'react'; + +import { favouritesActions } from '../store/favouritesSlice'; +import { + selectFavourites, + useCatsDispatch, + useCatsSelector, +} from '../store/hooks'; + +import C from './constants'; + +const useFavouritesPersistence = () => { + const dispatch = useCatsDispatch(); + const favourites = useCatsSelector(selectFavourites); + + useEffect(() => { + const savedData = localStorage.getItem(C.STORAGE_KEY_FAVOURITES); + if (savedData) { + Promise.resolve() + .then(() => JSON.parse(savedData)) + .then((parsed) => { + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + dispatch(favouritesActions.loadFavourites(parsed)); + } else { + console.warn(C.FAVOURITES_INVALID_FORMAT); + localStorage.removeItem(C.STORAGE_KEY_FAVOURITES); + } + }) + .catch((error) => { + console.error(C.FAVOURITES_LOAD_ERROR, error); + }); + } + }, [dispatch]); + + useEffect(() => { + Promise.resolve() + .then(() => + localStorage.setItem( + C.STORAGE_KEY_FAVOURITES, + JSON.stringify(favourites) + ) + ) + .catch((error) => { + console.error(C.FAVOURITES_SAVE_ERROR, error); + }); + }, [favourites]); +}; + +export default useFavouritesPersistence; diff --git a/src/index.css b/src/index.css new file mode 100644 index 00000000..72a58ca5 --- /dev/null +++ b/src/index.css @@ -0,0 +1,74 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + --scale: 0.2; +} + +html { + overflow-x: hidden; + font-size: 70%; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + overflow-x: hidden; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 00000000..5e88c23f --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,15 @@ +import { createRoot } from 'react-dom/client'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; + +import './index.css'; +import App from './App'; +import store from './store'; + +createRoot(document.getElementById('root')!).render( + + + + + +); diff --git a/src/router.tsx b/src/router.tsx new file mode 100644 index 00000000..2c9a2345 --- /dev/null +++ b/src/router.tsx @@ -0,0 +1,25 @@ +import { Navigate, Route, Routes } from 'react-router-dom'; + +import { BreedModal, Modal } from './components'; +import { Analytics, Breeds, Favourites, Gallery } from './views'; + +const AppRouter = () => { + return ( + + }> + } /> + + }> + } /> + } /> + + }> + } /> + + } /> + } /> + + ); +}; + +export default AppRouter; diff --git a/src/store/analyticsSlice.ts b/src/store/analyticsSlice.ts new file mode 100644 index 00000000..cd73c1a4 --- /dev/null +++ b/src/store/analyticsSlice.ts @@ -0,0 +1,33 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; + +import type { AnalyticsState } from './types'; + +const initialAnalyticsState: AnalyticsState = { + galleryCats: 0, + breedsViewed: {}, +}; + +const analyticsSlice = createSlice({ + name: 'analytics', + initialState: initialAnalyticsState, + reducers: { + incrementGalleryCats: (state, action: PayloadAction) => { + state.galleryCats += action.payload; + }, + incrementBreedView: ( + state, + action: PayloadAction<{ breedName: string }> + ) => { + const breedName = action.payload.breedName; + state.breedsViewed[breedName] = (state.breedsViewed[breedName] || 0) + 1; + }, + loadAnalytics: (_state, action: PayloadAction) => { + return action.payload; + }, + resetAnalytics: () => initialAnalyticsState, + }, +}); + +export const analyticsActions = analyticsSlice.actions; +export default analyticsSlice.reducer; diff --git a/src/store/breedsSlice.ts b/src/store/breedsSlice.ts new file mode 100644 index 00000000..1fd8f680 --- /dev/null +++ b/src/store/breedsSlice.ts @@ -0,0 +1,35 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; + +import type { Breed } from '../types'; + +interface BreedsStateWithMeta { + data: { [key: string]: Breed }; + lastFetched: number | null; +} + +const initialBreedsState: BreedsStateWithMeta = { + data: {}, + lastFetched: null, +}; + +const breedsSlice = createSlice({ + name: 'breeds', + initialState: initialBreedsState, + reducers: { + addBreeds: (state, action: PayloadAction) => { + action.payload.forEach((breed) => { + state.data[breed.id] = breed; + }); + state.lastFetched = Date.now(); + }, + loadBreeds: (_state, action: PayloadAction) => { + return action.payload; + }, + clearBreeds: () => initialBreedsState, + }, +}); + +export const breedsActions = breedsSlice.actions; +export default breedsSlice.reducer; +export type { BreedsStateWithMeta }; diff --git a/src/store/favouritesSlice.ts b/src/store/favouritesSlice.ts new file mode 100644 index 00000000..bbd77fd8 --- /dev/null +++ b/src/store/favouritesSlice.ts @@ -0,0 +1,29 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; + +import type { Cat } from '../types'; +import type { FavouritesState } from './types'; + +const initialFavouritesState: FavouritesState = {}; + +const favouritesSlice = createSlice({ + name: 'favourites', + initialState: initialFavouritesState, + reducers: { + toggleFavourite: (state, action: PayloadAction) => { + const cat = action.payload; + if (state[cat.id]) { + delete state[cat.id]; + } else { + state[cat.id] = cat; + } + }, + loadFavourites: (_state, action: PayloadAction) => { + return action.payload; + }, + clearFavourites: () => initialFavouritesState, + }, +}); + +export const favouritesActions = favouritesSlice.actions; +export default favouritesSlice.reducer; diff --git a/src/store/gallerySlice.ts b/src/store/gallerySlice.ts new file mode 100644 index 00000000..5fbb7410 --- /dev/null +++ b/src/store/gallerySlice.ts @@ -0,0 +1,25 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; + +import type { Cat } from '../types'; +import type { CatsState } from './types'; + +const initialGalleryState: CatsState = {}; + +const gallerySlice = createSlice({ + name: 'gallery', + initialState: initialGalleryState, + reducers: { + addCats: (state, action: PayloadAction) => { + const newState = { ...state }; + action.payload.forEach((cat) => { + newState[cat.id] = cat; + }); + return newState; + }, + clearCats: () => initialGalleryState, + }, +}); + +export const galleryActions = gallerySlice.actions; +export default gallerySlice.reducer; diff --git a/src/store/hooks.ts b/src/store/hooks.ts new file mode 100644 index 00000000..624e52fb --- /dev/null +++ b/src/store/hooks.ts @@ -0,0 +1,20 @@ +import { + type TypedUseSelectorHook, + useDispatch, + useSelector, +} from 'react-redux'; + +import type { AppDispatch, RootState } from './index'; + +export const useCatsDispatch = useDispatch.withTypes(); +export const useCatsSelector: TypedUseSelectorHook = useSelector; + +export const selectFavourites = (state: RootState) => state.favourites; + +export const selectGallery = (state: RootState) => state.gallery; + +export const selectBreeds = (state: RootState) => state.breeds.data; + +export const selectBreedsWithMeta = (state: RootState) => state.breeds; + +export const selectAnalytics = (state: RootState) => state.analytics; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 00000000..0f11437f --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,19 @@ +import { configureStore } from '@reduxjs/toolkit'; +import analyticsReducer from './analyticsSlice'; +import breedsReducer from './breedsSlice'; +import favouritesReducer from './favouritesSlice'; +import galleryReducer from './gallerySlice'; + +const store = configureStore({ + reducer: { + gallery: galleryReducer, + favourites: favouritesReducer, + breeds: breedsReducer, + analytics: analyticsReducer, + }, +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; + +export default store; diff --git a/src/store/types.ts b/src/store/types.ts new file mode 100644 index 00000000..abd2907e --- /dev/null +++ b/src/store/types.ts @@ -0,0 +1,16 @@ +import type { Breed, Cat } from '../types'; + +export type FavouritesState = Record; + +export type CatsState = Record; + +export type BreedsState = Record; + +export type BreedsViewed = { + [breedName: string]: number; +}; + +export type AnalyticsState = { + galleryCats: number; + breedsViewed: BreedsViewed; +}; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..70cd4fad --- /dev/null +++ b/src/types.ts @@ -0,0 +1,24 @@ +export type Cat = { + id: string; + url: string; + width: number; + height: number; + breeds?: Breed[]; +}; + +export type Breed = { + weight: { + imperial: string; + metric: string; + }; + id: string; + name: string; + temperament: string; + origin: string; + description: string; + life_span: string; + affection_level: number; + child_friendly: number; + dog_friendly: number; + image: Cat; +}; diff --git a/src/views/analytics/Analytics.module.css b/src/views/analytics/Analytics.module.css new file mode 100644 index 00000000..67d457c5 --- /dev/null +++ b/src/views/analytics/Analytics.module.css @@ -0,0 +1,107 @@ +.container { + max-width: 950px; + margin: 0 auto; + padding: 2rem; +} + +.title { + font-size: 2.5rem; + font-weight: 800; + color: #3d2817; + margin: 0 0 0.5rem 0; + text-align: center; +} + +.subtitle { + font-size: 1.125rem; + color: #5a4a3a; + text-align: center; + margin: 0 0 2rem 0; +} + +.statsGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.statCard { + background: #fff; + border: 3px solid; + border-radius: 1rem; + padding: 2rem; + text-align: center; + box-shadow: 0 4px 12px rgba(61, 40, 23, 0.1); + transition: transform 0.2s; +} + +.statCard:hover { + transform: translateY(-4px); + box-shadow: 0 8px 16px rgba(61, 40, 23, 0.15); +} + +.statValue { + font-size: 3rem; + font-weight: 800; + margin-bottom: 0.5rem; + line-height: 1; +} + +.statLabel { + font-size: 1rem; + color: #5a4a3a; + font-weight: 600; +} + +.chartsGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); + gap: 2rem; +} + +.chartContainer { + background: #fff; + border: 3px solid #3d2817; + border-radius: 1rem; + padding: 1.5rem; + box-shadow: 0 4px 12px rgba(61, 40, 23, 0.1); +} + +.chartTitle { + font-size: 1.5rem; + font-weight: 700; + color: #3d2817; + margin: 0 0 1.5rem 0; + text-align: center; +} + +.noData { + text-align: center; + color: #8b7355; + font-style: italic; + padding: 2rem; + font-size: 1.125rem; +} + +@media (max-width: 768px) { + .container { + padding: 1rem; + } + + .title { + font-size: 2rem; + } + + .statsGrid { + grid-template-columns: 1fr; + } + + .chartsGrid { + grid-template-columns: 1fr; + } + + .statValue { + font-size: 2.5rem; + } +} diff --git a/src/views/analytics/Analytics.tsx b/src/views/analytics/Analytics.tsx new file mode 100644 index 00000000..fd1a3fa2 --- /dev/null +++ b/src/views/analytics/Analytics.tsx @@ -0,0 +1,24 @@ +import { + BreedDistributionChart, + StatCards, + ViewsVsFavoritesChart, +} from './components'; + +import C from './constants'; +import styles from './Analytics.module.css'; + +const Analytics = () => { + return ( +
+

{C.TITLE}

+

{C.SUBTITLE}

+ +
+ + +
+
+ ); +}; + +export default Analytics; diff --git a/src/views/analytics/components/BreedDistributionChart.tsx b/src/views/analytics/components/BreedDistributionChart.tsx new file mode 100644 index 00000000..13b290c7 --- /dev/null +++ b/src/views/analytics/components/BreedDistributionChart.tsx @@ -0,0 +1,53 @@ +import { + Cell, + Legend, + Pie, + PieChart, + ResponsiveContainer, + Tooltip, +} from 'recharts'; +import useStats from './useStats'; + +import U from '../utils'; +import C from '../constants'; +import styles from '../Analytics.module.css'; + +const BreedDistributionChart = () => { + const { pieChartData } = useStats(); + if (!pieChartData.length) { + return ( +
+

{C.PIE_CHART_TITLE}

+

{C.PIE_CHART_EMPTY_TITLE}

+
+ ); + } + + return ( +
+

{C.PIE_CHART_TITLE}

+ + + U.getPieLabelText(name, percent)} + outerRadius={80} + fill="#8884d8" + dataKey="value" + > + {pieChartData.map((_, index) => ( + + ))} + + + + + +
+ ); +}; + +export default BreedDistributionChart; diff --git a/src/views/analytics/components/StatCards.tsx b/src/views/analytics/components/StatCards.tsx new file mode 100644 index 00000000..16543d30 --- /dev/null +++ b/src/views/analytics/components/StatCards.tsx @@ -0,0 +1,26 @@ +import useStats from './useStats'; + +import styles from '../Analytics.module.css'; + +const StatCards = () => { + const { stats } = useStats(); + + return ( +
+ {stats.map((stat) => ( +
+
+ {stat.value} +
+
{stat.label}
+
+ ))} +
+ ); +}; + +export default StatCards; diff --git a/src/views/analytics/components/ViewsVsFavoritesChart.tsx b/src/views/analytics/components/ViewsVsFavoritesChart.tsx new file mode 100644 index 00000000..78da605a --- /dev/null +++ b/src/views/analytics/components/ViewsVsFavoritesChart.tsx @@ -0,0 +1,36 @@ +import { + Bar, + BarChart, + CartesianGrid, + Legend, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import useStats from './useStats'; + +import C from '../constants'; +import styles from '../Analytics.module.css'; + +const ViewsVsFavoritesChart = () => { + const { barStats } = useStats(); + return ( +
+

{C.BAR_TITLE}

+ + + + + + + + + + + +
+ ); +}; + +export default ViewsVsFavoritesChart; diff --git a/src/views/analytics/components/index.ts b/src/views/analytics/components/index.ts new file mode 100644 index 00000000..9a2fc7db --- /dev/null +++ b/src/views/analytics/components/index.ts @@ -0,0 +1,3 @@ +export { default as BreedDistributionChart } from './BreedDistributionChart'; +export { default as StatCards } from './StatCards'; +export { default as ViewsVsFavoritesChart } from './ViewsVsFavoritesChart'; diff --git a/src/views/analytics/components/useStats.ts b/src/views/analytics/components/useStats.ts new file mode 100644 index 00000000..4601eb12 --- /dev/null +++ b/src/views/analytics/components/useStats.ts @@ -0,0 +1,44 @@ +import { useMemo } from 'react'; + +import { + selectAnalytics, + selectFavourites, + useCatsSelector, +} from '../../../store/hooks'; + +import U from '../utils'; + +const useStats = () => { + const analytics = useCatsSelector(selectAnalytics); + const favourites = useCatsSelector(selectFavourites); + + const totalImagesViewed: number = analytics?.galleryCats || 0; + const breedsViewed = analytics?.breedsViewed; + const totalFavourites: number = favourites + ? Object.keys(favourites).length + : 0; + + const favoriteRate = useMemo((): string => { + if (totalImagesViewed === 0) return '0'; + return ((totalFavourites / totalImagesViewed) * 100).toFixed(2); + }, [totalImagesViewed, totalFavourites]); + + const stats = useMemo( + () => U.formatStats(totalImagesViewed, totalFavourites, favoriteRate), + [totalImagesViewed, totalFavourites, favoriteRate] + ); + + const barStats = useMemo( + () => U.formatBarStats(totalImagesViewed, totalFavourites), + [totalImagesViewed, totalFavourites] + ); + + const pieChartData = useMemo( + () => U.formatPieChartData(breedsViewed), + [breedsViewed] + ); + + return { barStats, stats, pieChartData }; +}; + +export default useStats; diff --git a/src/views/analytics/constants.ts b/src/views/analytics/constants.ts new file mode 100644 index 00000000..3fdf1638 --- /dev/null +++ b/src/views/analytics/constants.ts @@ -0,0 +1,48 @@ +const BAR_COLOR_1 = '#fcc76f'; +const BAR_COLOR_2 = '#ff6b9d'; +const BAR_TITLE = 'Views vs Favorites'; +const CARD_COLOR_1 = '#fcc76f'; +const CARD_COLOR_2 = '#ff6b9d'; +const CARD_COLOR_3 = '#fbb94e'; +const FAVOURITES = 'Favourites'; +const FAVOURITES_RATE = 'Favourites Rate'; +const MAX_BREEDS_DISPLAYED = 6; +const NAME = 'name'; +const PIE_CHART_COLORS = [ + '#fcc76f', + '#ff6b9d', + '#fbb94e', + '#3d2817', + '#fce8b8', + '#8b7355', +]; +const PIE_CHART_EMPTY_TITLE = 'No breed data yet. Start exploring!'; +const PIE_CHART_TITLE = 'Most Viewed Breeds'; +const SUBTITLE = 'Track your cat viewing and favoriting activity'; +const TITLE = 'Analytics Dashboard'; +const TOTAL = 'Total'; +const TOTAL_FAVOURITES = 'Total Favourites'; +const TOTAL_IMAGES_VIEWED = 'Total Images Viewed'; +const VIEWS = 'Views'; + +export default { + BAR_COLOR_1, + BAR_COLOR_2, + BAR_TITLE, + CARD_COLOR_1, + CARD_COLOR_2, + CARD_COLOR_3, + FAVOURITES, + FAVOURITES_RATE, + MAX_BREEDS_DISPLAYED, + NAME, + PIE_CHART_COLORS, + PIE_CHART_EMPTY_TITLE, + PIE_CHART_TITLE, + SUBTITLE, + TITLE, + TOTAL, + TOTAL_FAVOURITES, + TOTAL_IMAGES_VIEWED, + VIEWS, +}; diff --git a/src/views/analytics/index.ts b/src/views/analytics/index.ts new file mode 100644 index 00000000..2cba8b9f --- /dev/null +++ b/src/views/analytics/index.ts @@ -0,0 +1 @@ +export { default } from './Analytics'; diff --git a/src/views/analytics/utils.ts b/src/views/analytics/utils.ts new file mode 100644 index 00000000..606d9681 --- /dev/null +++ b/src/views/analytics/utils.ts @@ -0,0 +1,84 @@ +import C from './constants'; + +type StatItem = { + label: string; + value: number | string; + color: string; +}; + +type BarChartItem = { + name: string; + Views: number; + Favourites: number; +}; + +type PieChartItem = { + name: string; + value: number; +}; + +const formatStats = ( + totalImagesViewed: number, + totalFavourites: number, + favoriteRate: string +): StatItem[] => { + return [ + { + label: C.TOTAL_IMAGES_VIEWED, + value: totalImagesViewed, + color: C.CARD_COLOR_1, + }, + { + label: C.TOTAL_FAVOURITES, + value: totalFavourites, + color: C.CARD_COLOR_2, + }, + { + label: C.FAVOURITES_RATE, + value: `${favoriteRate}%`, + color: C.CARD_COLOR_3, + }, + ]; +}; + +const formatBarStats = ( + totalImagesViewed: number, + totalFavourites: number +): BarChartItem[] => { + return [ + { + name: C.TOTAL, + Views: totalImagesViewed, + Favourites: totalFavourites, + }, + ]; +}; + +const getPieLabelText = ( + name: string | undefined, + percent: number | undefined +): string => `${name} ${((percent || 0) * 100).toFixed(2)}%`; + +const getPieFillColor = (index: number): string => + C.PIE_CHART_COLORS[index % C.PIE_CHART_COLORS.length]; + +const formatPieChartData = ( + breedsViewed?: Record +): PieChartItem[] => { + if (!breedsViewed) return []; + return Object.entries(breedsViewed) + .map(([breedName, count]) => ({ + name: breedName, + value: count as number, + })) + .sort((a, b) => b.value - a.value) + .slice(0, C.MAX_BREEDS_DISPLAYED); +}; + +export default { + formatBarStats, + formatStats, + getPieLabelText, + getPieFillColor, + formatPieChartData, +}; diff --git a/src/views/breeds/Breeds.tsx b/src/views/breeds/Breeds.tsx new file mode 100644 index 00000000..d351340d --- /dev/null +++ b/src/views/breeds/Breeds.tsx @@ -0,0 +1,44 @@ +import { useEffect } from 'react'; +import { Outlet } from 'react-router-dom'; + +import useBreeds from './useBreeds'; +import { Card, ErrorMessage, LoadingSpinner } from '../../components'; + +import U from './utils'; +import C from './constants'; +import layoutStyles from '../shared/viewsLayout.module.css'; + +const Breeds = () => { + const { cachedBreeds, areBreedsEmpty, isInitialLoading, error, loadBreeds } = + useBreeds(); + + useEffect(() => { + if (areBreedsEmpty) { + loadBreeds(); + } + }, [areBreedsEmpty, loadBreeds]); + + if (isInitialLoading) { + return ; + } + + return ( +
+

{C.TITLE_TEXT}

+
+ {cachedBreeds.map((breed) => ( + + ))} +
+ {error && } + +
+ ); +}; + +export default Breeds; diff --git a/src/views/breeds/constants.ts b/src/views/breeds/constants.ts new file mode 100644 index 00000000..eee86956 --- /dev/null +++ b/src/views/breeds/constants.ts @@ -0,0 +1,9 @@ +const TITLE_TEXT = 'Cat Breeds'; +const ERROR_MESSAGE = 'Failed to load breeds. Please try again.'; +const ONE_DAY_MS = 24 * 60 * 60 * 1000; + +export default { + ERROR_MESSAGE, + ONE_DAY_MS, + TITLE_TEXT, +}; diff --git a/src/views/breeds/index.ts b/src/views/breeds/index.ts new file mode 100644 index 00000000..984072ce --- /dev/null +++ b/src/views/breeds/index.ts @@ -0,0 +1 @@ +export { default } from './Breeds'; diff --git a/src/views/breeds/useBreeds.ts b/src/views/breeds/useBreeds.ts new file mode 100644 index 00000000..ed2f8ba2 --- /dev/null +++ b/src/views/breeds/useBreeds.ts @@ -0,0 +1,59 @@ +import { useCallback, useState } from 'react'; + +import { fetchAllBreeds } from '../../api'; +import { breedsActions } from '../../store/breedsSlice'; +import { + selectBreeds, + selectBreedsWithMeta, + useCatsDispatch, + useCatsSelector, +} from '../../store/hooks'; + +import U from './utils'; +import C from './constants'; +import type { Breed } from '../../types'; + +const useBreeds = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const dispatch = useCatsDispatch(); + const cachedBreeds: Breed[] = Object.values(useCatsSelector(selectBreeds)); + const breedsWithMeta = useCatsSelector(selectBreedsWithMeta); + const lastFetched = breedsWithMeta?.lastFetched; + + const areBreedsEmpty: boolean = cachedBreeds.length === 0; + const isInitialLoading: boolean = areBreedsEmpty && isLoading; + const shouldFetch = U.shouldFetchBreeds(areBreedsEmpty, lastFetched); + + const loadBreeds = useCallback(() => { + if (!shouldFetch) { + return; + } + + setIsLoading(true); + setError(null); + + fetchAllBreeds() + .then((breeds: Breed[]) => { + dispatch(breedsActions.addBreeds(breeds)); + }) + .catch((error) => { + setError(C.ERROR_MESSAGE); + console.error(error); + }) + .finally(() => { + setIsLoading(false); + }); + }, [dispatch, shouldFetch]); + + return { + cachedBreeds, + areBreedsEmpty, + isInitialLoading, + error, + loadBreeds, + }; +}; + +export default useBreeds; diff --git a/src/views/breeds/utils.ts b/src/views/breeds/utils.ts new file mode 100644 index 00000000..f13e245e --- /dev/null +++ b/src/views/breeds/utils.ts @@ -0,0 +1,23 @@ +import C from './constants'; +import type { Breed, Cat } from '../../types'; + +const transformBreedToCatObject = (breed: Breed): Cat => { + return { + ...breed.image, + breeds: [breed], + }; +}; + +const shouldFetchBreeds = ( + isDataEmpty: boolean, + lastFetched: number | null +): boolean => { + if (isDataEmpty) return true; + if (!lastFetched) return true; + + const now = Date.now(); + const daysSinceLastFetch = (now - lastFetched) / C.ONE_DAY_MS; + return daysSinceLastFetch >= 1; +}; + +export default { shouldFetchBreeds, transformBreedToCatObject }; diff --git a/src/views/favourites/Favourites.module.css b/src/views/favourites/Favourites.module.css new file mode 100644 index 00000000..e45e2643 --- /dev/null +++ b/src/views/favourites/Favourites.module.css @@ -0,0 +1,7 @@ +.emptyMessage { + text-align: center; + color: #8b7355; + font-size: 1.2rem; + margin: 3rem 0; + font-weight: 500; +} diff --git a/src/views/favourites/Favourites.tsx b/src/views/favourites/Favourites.tsx new file mode 100644 index 00000000..cf7f511f --- /dev/null +++ b/src/views/favourites/Favourites.tsx @@ -0,0 +1,36 @@ +import { Outlet } from 'react-router-dom'; + +import useFavourites from './useFavourites'; +import Card from '../../components/card'; + +import C from './constants'; +import layoutStyles from '../shared/viewsLayout.module.css'; +import styles from './Favourites.module.css'; + +const Favourites = () => { + const { favouriteCats, isFavouritesEmpty } = useFavourites(); + + if (isFavouritesEmpty) { + return ( +
+

{C.TITLE_TEXT}

+

{C.EMPTY_MESSAGE}

+ +
+ ); + } + + return ( +
+

{C.TITLE_TEXT}

+
+ {favouriteCats.map((cat) => ( + + ))} +
+ +
+ ); +}; + +export default Favourites; diff --git a/src/views/favourites/constants.ts b/src/views/favourites/constants.ts new file mode 100644 index 00000000..1fbf2f2f --- /dev/null +++ b/src/views/favourites/constants.ts @@ -0,0 +1,7 @@ +const EMPTY_MESSAGE = 'No favourite cats yet. Start exploring and add some!'; +const TITLE_TEXT = 'Your Favourite Cats'; + +export default { + EMPTY_MESSAGE, + TITLE_TEXT, +}; diff --git a/src/views/favourites/index.ts b/src/views/favourites/index.ts new file mode 100644 index 00000000..112e85a1 --- /dev/null +++ b/src/views/favourites/index.ts @@ -0,0 +1 @@ +export { default } from './Favourites'; diff --git a/src/views/favourites/useFavourites.ts b/src/views/favourites/useFavourites.ts new file mode 100644 index 00000000..3f2a87ab --- /dev/null +++ b/src/views/favourites/useFavourites.ts @@ -0,0 +1,15 @@ +import { selectFavourites, useCatsSelector } from '../../store/hooks'; + +import type { Cat } from '../../types'; + +const useFavourites = () => { + const favouriteCats: Cat[] = Object.values(useCatsSelector(selectFavourites)); + const isFavouritesEmpty: boolean = favouriteCats.length === 0; + + return { + favouriteCats, + isFavouritesEmpty, + }; +}; + +export default useFavourites; diff --git a/src/views/gallery/Gallery.module.css b/src/views/gallery/Gallery.module.css new file mode 100644 index 00000000..3bcd6d11 --- /dev/null +++ b/src/views/gallery/Gallery.module.css @@ -0,0 +1,32 @@ +.buttonRow { + display: flex; + justify-content: center; + gap: 1.5rem; + margin: 2rem 0; +} + +.button { + padding: 1rem 2.5rem; + font-size: 1.1rem; + background: #fcc76f; + color: #3d2817; + border: none; + border-radius: 2rem; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 4px 8px rgba(61, 40, 23, 0.15); + font-weight: 600; +} + +.button:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 6px 12px rgba(61, 40, 23, 0.2); + background: #fbb94e; +} + +.button:disabled { + background: #e5d5b8; + cursor: not-allowed; + box-shadow: none; + color: #8b7355; +} diff --git a/src/views/gallery/Gallery.tsx b/src/views/gallery/Gallery.tsx new file mode 100644 index 00000000..6b0212b0 --- /dev/null +++ b/src/views/gallery/Gallery.tsx @@ -0,0 +1,58 @@ +import { useEffect } from 'react'; +import { Outlet } from 'react-router-dom'; + +import GalleryButtons from './GalleryButtons'; +import useGallery from './useGallery'; +import { Card } from '../../components'; +import { ErrorMessage, LoadingSpinner } from '../../components'; + +import C from './constants'; +import layoutStyles from '../shared/viewsLayout.module.css'; + +const Gallery = () => { + const { + cachedCats, + favouriteCats, + isInitialLoading, + isLoading, + isGalleryEmpty, + error, + loadCats, + clearCats, + } = useGallery(); + + useEffect(() => { + if (isGalleryEmpty) { + loadCats(); + } + }, []); + + if (isInitialLoading) { + return ; + } + + return ( +
+

{C.TITLE_TEXT}

+
+ {cachedCats.map((cat) => ( + + ))} +
+ {error && } + + +
+ ); +}; + +export default Gallery; diff --git a/src/views/gallery/GalleryButtons.tsx b/src/views/gallery/GalleryButtons.tsx new file mode 100644 index 00000000..e913b55b --- /dev/null +++ b/src/views/gallery/GalleryButtons.tsx @@ -0,0 +1,38 @@ +import C from './constants'; +import styles from './Gallery.module.css'; + +type GalleryButtonsProps = { + isLoading: boolean; + loadCats: () => void; + clearCats: () => void; +}; + +const GalleryButtons = ({ + isLoading, + loadCats, + clearCats, +}: GalleryButtonsProps) => { + return ( +
+ + +
+ ); +}; + +export default GalleryButtons; diff --git a/src/views/gallery/constants.ts b/src/views/gallery/constants.ts new file mode 100644 index 00000000..12e47f15 --- /dev/null +++ b/src/views/gallery/constants.ts @@ -0,0 +1,13 @@ +const CLEAR_BUTTON_TEXT = 'Clear cats'; +const ERROR_MESSAGE = 'Failed to load cats. Please try again'; +const LOAD_BUTTON_TEXT = 'Load more cats'; +const NUMBER_OF_CATS_TO_BE_FETCHED = 10; +const TITLE_TEXT = 'Meet the cats'; + +export default { + CLEAR_BUTTON_TEXT, + ERROR_MESSAGE, + LOAD_BUTTON_TEXT, + NUMBER_OF_CATS_TO_BE_FETCHED, + TITLE_TEXT, +}; diff --git a/src/views/gallery/index.ts b/src/views/gallery/index.ts new file mode 100644 index 00000000..cde3adaa --- /dev/null +++ b/src/views/gallery/index.ts @@ -0,0 +1 @@ +export { default } from './Gallery'; diff --git a/src/views/gallery/useGallery.ts b/src/views/gallery/useGallery.ts new file mode 100644 index 00000000..1c13be27 --- /dev/null +++ b/src/views/gallery/useGallery.ts @@ -0,0 +1,66 @@ +import { useCallback, useState } from 'react'; + +import { fetchRandomCats } from '../../api'; +import { analyticsActions } from '../../store/analyticsSlice'; +import { galleryActions } from '../../store/gallerySlice'; +import { useCatsDispatch, useCatsSelector } from '../../store/hooks'; +import { selectFavourites, selectGallery } from '../../store/hooks'; + +import C from './constants'; +import type { Cat } from '../../types'; + +const useGallery = () => { + const [cats, setCats] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const dispatch = useCatsDispatch(); + + const cachedCats: Cat[] = Object.values(useCatsSelector(selectGallery)); + const favouriteCats: Record = useCatsSelector(selectFavourites); + + const isGalleryEmpty = cachedCats.length === 0; + const isInitialLoading = isLoading && isGalleryEmpty; + + const loadCats = useCallback(() => { + setIsLoading(true); + setError(null); + + fetchRandomCats(C.NUMBER_OF_CATS_TO_BE_FETCHED) + .then((newCats) => { + const existingCats = new Set(cats.map((cat) => cat.id)); + const uniqueNewCats = newCats.filter( + (cat) => !existingCats.has(cat.id) + ); + + dispatch(galleryActions.addCats(uniqueNewCats)); + dispatch(analyticsActions.incrementGalleryCats(uniqueNewCats.length)); + setCats((prev) => [...prev, ...uniqueNewCats]); + }) + .catch((err) => { + setError(C.ERROR_MESSAGE); + console.error(err); + }) + .finally(() => { + setIsLoading(false); + }); + }, [cats, dispatch]); + + const clearCats = useCallback(() => { + dispatch(galleryActions.clearCats()); + setCats([]); + }, [dispatch]); + + return { + cachedCats, + favouriteCats, + isInitialLoading, + isLoading, + isGalleryEmpty, + error, + loadCats, + clearCats, + }; +}; + +export default useGallery; diff --git a/src/views/index.ts b/src/views/index.ts new file mode 100644 index 00000000..a09dc5bb --- /dev/null +++ b/src/views/index.ts @@ -0,0 +1,4 @@ +export { default as Analytics } from './analytics'; +export { default as Breeds } from './breeds'; +export { default as Favourites } from './favourites'; +export { default as Gallery } from './gallery'; diff --git a/src/views/shared/viewsLayout.module.css b/src/views/shared/viewsLayout.module.css new file mode 100644 index 00000000..23203253 --- /dev/null +++ b/src/views/shared/viewsLayout.module.css @@ -0,0 +1,34 @@ +.container { + padding: 2rem; + max-width: 900px; + margin: 0 auto; + background: transparent; +} + +.title { + margin-bottom: 2rem; + text-align: center; + color: #3d2817; + font-weight: 800; + font-size: 2rem; +} + +.grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; + margin-bottom: 2rem; +} + +@media (max-width: 768px) { + .grid { + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + } +} + +@media (max-width: 480px) { + .grid { + grid-template-columns: 1fr; + } +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 00000000..a9b5a59c --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..1ffef600 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 00000000..8a67f62f --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vercel.json b/vercel.json new file mode 100644 index 00000000..1323cdac --- /dev/null +++ b/vercel.json @@ -0,0 +1,8 @@ +{ + "rewrites": [ + { + "source": "/(.*)", + "destination": "/index.html" + } + ] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 00000000..8b0f57b9 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +})