From af0608ed43402230082fa1f874e4994f7a9ded97 Mon Sep 17 00:00:00 2001 From: 2ManyProjects Date: Fri, 25 Apr 2025 00:41:30 -0500 Subject: [PATCH] Git Migration --- .gitignore | 6 + backend/.gitignore | 5 + backend/Dockerfile | 14 + backend/README.md | 92 + backend/package.json | 25 + backend/src/config.js | 38 + backend/src/db/index.js | 25 + backend/src/index.js | 226 + backend/src/middleware/adminAuth.js | 50 + backend/src/middleware/auth.js | 42 + backend/src/middleware/upload.js | 59 + backend/src/routes/auth.js | 271 ++ backend/src/routes/cart.js | 363 ++ backend/src/routes/categoryAdmin.js | 186 + backend/src/routes/images.js | 97 + backend/src/routes/productAdmin.js | 437 ++ backend/src/routes/productAdminImages.js | 41 + backend/src/routes/products.js | 177 + db/init/01-schema.sql | 170 + db/init/02-seed.sql | 191 + db/init/03-api-key.sql | 5 + db/init/04-product-images.sql | 44 + db/init/05-admin-role.sql | 10 + db/init/06-product-categories.sql | 1 + db/test/test-api.sh | 67 + docker-compose.yml | 66 + fileStructure.txt | 96 + frontend/Dockerfile | 58 + frontend/README.md | 66 + frontend/index.html | 14 + frontend/nginx.conf | 37 + frontend/package-lock.json | 3664 +++++++++++++++++ frontend/package.json | 38 + frontend/public/favicon.svg | 5 + frontend/setup-frontend.sh | 33 + frontend/src/App.jsx | 85 + frontend/src/components/Footer.jsx | 86 + frontend/src/components/ImageUploader.jsx | 225 + frontend/src/components/Notifications.jsx | 39 + frontend/src/components/ProductImage.jsx | 67 + frontend/src/components/ProtectedRoute.jsx | 48 + frontend/src/config.js | 21 + frontend/src/features/auth/authSlice.js | 59 + frontend/src/features/cart/cartSlice.js | 58 + frontend/src/features/ui/uiSlice.js | 59 + frontend/src/hooks/apiHooks.js | 271 ++ frontend/src/hooks/categoryAdminHooks.js | 98 + frontend/src/hooks/reduxHooks.js | 105 + frontend/src/layouts/AdminLayout.jsx | 214 + frontend/src/layouts/AuthLayout.jsx | 69 + frontend/src/layouts/MainLayout.jsx | 219 + frontend/src/main.jsx | 42 + frontend/src/pages/Admin/CategoriesPage.jsx | 429 ++ frontend/src/pages/Admin/DashboardPage.jsx | 294 ++ frontend/src/pages/Admin/ProductEditPage.jsx | 614 +++ frontend/src/pages/Admin/ProductsPage.jsx | 308 ++ frontend/src/pages/CartPage.jsx | 331 ++ frontend/src/pages/CheckoutPage.jsx | 441 ++ frontend/src/pages/HomePage.jsx | 168 + frontend/src/pages/LoginPage.jsx | 172 + frontend/src/pages/NotFoundPage.jsx | 59 + frontend/src/pages/ProductDetailPage.jsx | 360 ++ frontend/src/pages/ProductsPage.jsx | 557 +++ frontend/src/pages/RegisterPage.jsx | 133 + frontend/src/pages/VerifyPage.jsx | 119 + frontend/src/services/api.js | 42 + frontend/src/services/authService.js | 80 + frontend/src/services/cartService.js | 83 + frontend/src/services/categoryAdminService.js | 81 + frontend/src/services/imageService.js | 85 + frontend/src/services/productService.js | 124 + frontend/src/store/index.js | 26 + frontend/src/theme/ThemeProvider.jsx | 21 + frontend/src/theme/index.js | 106 + frontend/src/utils/imageUtils.js | 163 + frontend/vite.config.js | 48 + 76 files changed, 13328 insertions(+) create mode 100644 .gitignore create mode 100644 backend/.gitignore create mode 100644 backend/Dockerfile create mode 100644 backend/README.md create mode 100644 backend/package.json create mode 100644 backend/src/config.js create mode 100644 backend/src/db/index.js create mode 100644 backend/src/index.js create mode 100644 backend/src/middleware/adminAuth.js create mode 100644 backend/src/middleware/auth.js create mode 100644 backend/src/middleware/upload.js create mode 100644 backend/src/routes/auth.js create mode 100644 backend/src/routes/cart.js create mode 100644 backend/src/routes/categoryAdmin.js create mode 100644 backend/src/routes/images.js create mode 100644 backend/src/routes/productAdmin.js create mode 100644 backend/src/routes/productAdminImages.js create mode 100644 backend/src/routes/products.js create mode 100644 db/init/01-schema.sql create mode 100644 db/init/02-seed.sql create mode 100644 db/init/03-api-key.sql create mode 100644 db/init/04-product-images.sql create mode 100644 db/init/05-admin-role.sql create mode 100644 db/init/06-product-categories.sql create mode 100644 db/test/test-api.sh create mode 100644 docker-compose.yml create mode 100644 fileStructure.txt create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/index.html create mode 100644 frontend/nginx.conf create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/setup-frontend.sh create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/components/Footer.jsx create mode 100644 frontend/src/components/ImageUploader.jsx create mode 100644 frontend/src/components/Notifications.jsx create mode 100644 frontend/src/components/ProductImage.jsx create mode 100644 frontend/src/components/ProtectedRoute.jsx create mode 100644 frontend/src/config.js create mode 100644 frontend/src/features/auth/authSlice.js create mode 100644 frontend/src/features/cart/cartSlice.js create mode 100644 frontend/src/features/ui/uiSlice.js create mode 100644 frontend/src/hooks/apiHooks.js create mode 100644 frontend/src/hooks/categoryAdminHooks.js create mode 100644 frontend/src/hooks/reduxHooks.js create mode 100644 frontend/src/layouts/AdminLayout.jsx create mode 100644 frontend/src/layouts/AuthLayout.jsx create mode 100644 frontend/src/layouts/MainLayout.jsx create mode 100644 frontend/src/main.jsx create mode 100644 frontend/src/pages/Admin/CategoriesPage.jsx create mode 100644 frontend/src/pages/Admin/DashboardPage.jsx create mode 100644 frontend/src/pages/Admin/ProductEditPage.jsx create mode 100644 frontend/src/pages/Admin/ProductsPage.jsx create mode 100644 frontend/src/pages/CartPage.jsx create mode 100644 frontend/src/pages/CheckoutPage.jsx create mode 100644 frontend/src/pages/HomePage.jsx create mode 100644 frontend/src/pages/LoginPage.jsx create mode 100644 frontend/src/pages/NotFoundPage.jsx create mode 100644 frontend/src/pages/ProductDetailPage.jsx create mode 100644 frontend/src/pages/ProductsPage.jsx create mode 100644 frontend/src/pages/RegisterPage.jsx create mode 100644 frontend/src/pages/VerifyPage.jsx create mode 100644 frontend/src/services/api.js create mode 100644 frontend/src/services/authService.js create mode 100644 frontend/src/services/cartService.js create mode 100644 frontend/src/services/categoryAdminService.js create mode 100644 frontend/src/services/imageService.js create mode 100644 frontend/src/services/productService.js create mode 100644 frontend/src/store/index.js create mode 100644 frontend/src/theme/ThemeProvider.jsx create mode 100644 frontend/src/theme/index.js create mode 100644 frontend/src/utils/imageUtils.js create mode 100644 frontend/vite.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..459cf4c --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +.env +npm-debug.log +yarn-error.log +.DS_Store +uploads/* \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..ee39897 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,5 @@ +node_modules +.env +npm-debug.log +yarn-error.log +.DS_Store \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..19f0f6f --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,14 @@ +FROM node:18-alpine + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +RUN mkdir -p public/uploads/products +COPY . . + +EXPOSE 4000 + +CMD ["npm", "start"] \ No newline at end of file diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..46f138a --- /dev/null +++ b/backend/README.md @@ -0,0 +1,92 @@ +# E-commerce API Backend + +API backend for the Rocks, Bones & Sticks e-commerce platform. + +## Setup + +```bash +# Install dependencies +npm install + +# Run for development +npm run dev + +# Run for production +npm start +``` + +## API Endpoints + +### Authentication + +- `POST /api/auth/register` - Register a new user +- `POST /api/auth/login-request` - Request a login code +- `POST /api/auth/verify` - Verify login code and generate API key +- `POST /api/auth/verify-key` - Verify an existing API key +- `POST /api/auth/logout` - Logout current user and invalidate API key + +For protected routes, include the API key in the request header: +``` +X-API-Key: your-api-key-here +``` + +### Products + +- `GET /api/products` - Get all products +- `GET /api/products/:id` - Get single product +- `GET /api/products/categories/all` - Get all categories +- `GET /api/products/tags/all` - Get all tags +- `GET /api/products/category/:categoryName` - Get products by category + + +### Product Admin (Admin Protected) + +These routes require an API key with admin privileges. + +- `POST /api/admin/products` - Create a new product with multiple images +- `PUT /api/admin/products/:id` - Update a product +- `DELETE /api/admin/products/:id` - Delete a product + + +### Cart (Protected) + +- `GET /api/cart/:userId` - Get users cart +- `POST /api/cart/add` - Add item to cart +- `PUT /api/cart/update` - Update cart item quantity +- `DELETE /api/cart/clear/:userId` - Clear cart +- `POST /api/cart/checkout` - Checkout (create order from cart) + +## Admin Access + +By default, the user with email `john@example.com` is set as an admin for testing purposes. The admin status allows access to protected admin routes. + +## Environment Variables + +Create a `.env` file with the following variables: + +``` +# Server configuration +PORT=4000 +NODE_ENV=development +ENVIRONMENT=beta # Use 'beta' for development, 'prod' for production + +# Database connection +DB_HOST=db +DB_USER=postgres +DB_PASSWORD=PLEASECHANGETOSECUREPASSWORD +DB_NAME=ecommerce +DB_PORT=5432 + +# Email configuration (Postmark) +EMAIL_HOST=smtp.postmarkapp.com +EMAIL_PORT=587 +EMAIL_USER=your_postmark_api_token +EMAIL_PASS=your_postmark_api_token +``` + +### Environment-specific Behavior + +Based on the `ENVIRONMENT` variable, the application will use different domain configurations: + +- `beta`: Uses `localhost:3000` for the frontend and `http` protocol +- `prod`: Uses `rocks.2many.ca` for the frontend and `https` protocol \ No newline at end of file diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..42bc4d2 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,25 @@ +{ + "name": "rocks-2many-backend", + "version": "1.0.0", + "description": "Backend API for rocks, bones, and sticks e-commerce store", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "nodemon src/index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.0.3", + "express": "^4.18.2", + "morgan": "^1.10.0", + "multer": "^1.4.5-lts.2", + "nodemailer": "^6.9.1", + "pg": "^8.10.0", + "pg-hstore": "^2.3.4", + "uuid": "^9.0.0" + }, + "devDependencies": { + "nodemon": "^2.0.22" + } +} diff --git a/backend/src/config.js b/backend/src/config.js new file mode 100644 index 0000000..80d0efd --- /dev/null +++ b/backend/src/config.js @@ -0,0 +1,38 @@ +const dotenv = require('dotenv'); + +// Load environment variables +dotenv.config(); + +const config = { + // Server configuration + port: process.env.PORT || 4000, + nodeEnv: process.env.NODE_ENV || 'development', + environment: process.env.ENVIRONMENT || 'beta', + + // Database configuration + db: { + host: process.env.DB_HOST || 'db', + user: process.env.DB_USER || 'postgres', + password: process.env.DB_PASSWORD || 'postgres', + database: process.env.DB_NAME || 'ecommerce', + port: process.env.DB_PORT || 5432 + }, + + // Email configuration + email: { + host: process.env.EMAIL_HOST || 'smtp.postmarkapp.com', + port: process.env.EMAIL_PORT || 587, + user: process.env.EMAIL_USER || '03c638d8-4a04-9be6', + pass: process.env.EMAIL_PASS || '03c638d8-4a04-9be6', + reply: process.env.EMAIL_REPLY || 'noreply@2many.ca' + }, + + // Site configuration (domain and protocol based on environment) + site: { + domain: process.env.ENVIRONMENT === 'prod' ? 'rocks.2many.ca' : 'localhost:3000', + protocol: process.env.ENVIRONMENT === 'prod' ? 'https' : 'http', + apiDomain: process.env.ENVIRONMENT === 'prod' ? 'api.rocks.2many.ca' : 'localhost:4000' + } +}; + +module.exports = config; \ No newline at end of file diff --git a/backend/src/db/index.js b/backend/src/db/index.js new file mode 100644 index 0000000..072641e --- /dev/null +++ b/backend/src/db/index.js @@ -0,0 +1,25 @@ +const { Pool } = require('pg'); +const config = require('../config') + +// Create a pool instance +const pool = new Pool({ + user: config.db.user, + password: config.db.password, + host: config.db.host, + port: config.db.port, + database: config.db.database +}); + +// Helper function for running queries +const query = async (text, params) => { + const start = Date.now(); + const res = await pool.query(text, params); + const duration = Date.now() - start; + console.log('Executed query', { text, duration, rows: res.rowCount }); + return res; +}; + +module.exports = { + query, + pool +}; \ No newline at end of file diff --git a/backend/src/index.js b/backend/src/index.js new file mode 100644 index 0000000..53f29fa --- /dev/null +++ b/backend/src/index.js @@ -0,0 +1,226 @@ +const express = require('express'); +const multer = require('multer'); +const path = require('path'); +const cors = require('cors'); +const morgan = require('morgan'); +const config = require('./config'); +const { query, pool } = require('./db') +const authMiddleware = require('./middleware/auth'); +const adminAuthMiddleware = require('./middleware/adminAuth'); +const fs = require('fs'); + +// routes +const productRoutes = require('./routes/products'); +const authRoutes = require('./routes/auth'); +const cartRoutes = require('./routes/cart'); +const productAdminRoutes = require('./routes/productAdmin'); +const categoryAdminRoutes = require('./routes/categoryAdmin'); // Add category admin routes +// Create Express app +const app = express(); +const port = config.port || 4000; + +// Ensure uploads directories exist +const uploadsDir = path.join(__dirname, '../public/uploads'); +const productImagesDir = path.join(uploadsDir, 'products'); + +if (!fs.existsSync(uploadsDir)) { + fs.mkdirSync(uploadsDir, { recursive: true }); +} + +if (!fs.existsSync(productImagesDir)) { + fs.mkdirSync(productImagesDir, { recursive: true }); +} + +// Configure storage +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + // Determine destination based on upload type + if (req.originalUrl.includes('/product')) { + cb(null, productImagesDir); + } else { + cb(null, uploadsDir); + } + }, + filename: (req, file, cb) => { + // Create unique filename with original extension + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + const fileExt = path.extname(file.originalname); + const safeName = path.basename(file.originalname, fileExt) + .toLowerCase() + .replace(/[^a-z0-9]/g, '-'); + + cb(null, `${safeName}-${uniqueSuffix}${fileExt}`); + } +}); + +// File filter to only allow images +const fileFilter = (req, file, cb) => { + // Accept only image files + if (file.mimetype.startsWith('image/')) { + cb(null, true); + } else { + cb(new Error('Only image files are allowed!'), false); + } +}; + +// Create the multer instance +const upload = multer({ + storage, + fileFilter, + limits: { + fileSize: 5 * 1024 * 1024 // 5MB limit + } +}); + +pool.connect() + .then(() => console.log('Connected to PostgreSQL database')) + .catch(err => console.error('Database connection error:', err)); + +// Middleware +app.use(cors()); +app.use(express.json()); +app.use(morgan('dev')); + +// Serve static files - serve the entire public directory +app.use(express.static(path.join(__dirname, '../public'))); + +// More specific static file serving for images +app.use('/images', express.static(path.join(__dirname, '../public/uploads'))); +app.use('/uploads', express.static(path.join(__dirname, '../public/uploads'))); +app.use('/api/uploads', express.static(path.join(__dirname, '../public/uploads'))); +app.use('/api/images', express.static(path.join(__dirname, '../public/uploads'))); +if (!fs.existsSync(path.join(__dirname, '../public/uploads'))) { + fs.mkdirSync(path.join(__dirname, '../public/uploads'), { recursive: true }); +} + +if (!fs.existsSync(path.join(__dirname, '../public/uploads/products'))) { + fs.mkdirSync(path.join(__dirname, '../public/uploads/products'), { recursive: true }); +} +// For direct access to product images +app.use('/products/images', express.static(path.join(__dirname, '../public/uploads/products'))); +app.use('/api/products/images', express.static(path.join(__dirname, '../public/uploads/products'))); +// Health check route +app.get('/health', (req, res) => { + res.status(200).json({ status: 'ok', message: 'API is running' }); +}); + +// Upload endpoints +// Public upload endpoint (basic) +app.post('/api/image/upload', upload.single('image'), (req, res) => { + console.log('/api/image/upload'); + if (!req.file) { + return res.status(400).json({ + error: true, + message: 'No image file provided' + }); + } + + res.json({ + success: true, + imagePath: `/uploads/${req.file.filename}` + }); +}); + +// Admin-only product image upload +app.post('/api/image/product', adminAuthMiddleware(pool, query), upload.single('image'), (req, res) => { + console.log('/api/image/product', req.file); + if (!req.file) { + return res.status(400).json({ + error: true, + message: 'No image file provided' + }); + } + + // Get the relative path to the image + const imagePath = `/uploads/products/${req.file.filename}`; + + res.json({ + success: true, + imagePath, + filename: req.file.filename + }); +}); + +// Upload multiple product images (admin only) +app.post('/api/image/products', adminAuthMiddleware(pool, query), upload.array('images', 10), (req, res) => { + console.log('/api/image/products', req.files); + if (!req.files || req.files.length === 0) { + return res.status(400).json({ + error: true, + message: 'No image files provided' + }); + } + + // Get the relative paths to the images + const imagePaths = req.files.map(file => ({ + imagePath: `/uploads/products/${file.filename}`, + filename: file.filename + })); + + res.json({ + success: true, + images: imagePaths + }); +}); + +// Delete product image (admin only) +app.delete('/api/image/product/:filename', adminAuthMiddleware(pool, query), (req, res) => { + try { + const { filename } = req.params; + + // Prevent path traversal attacks + if (filename.includes('..') || filename.includes('/')) { + return res.status(400).json({ + error: true, + message: 'Invalid filename' + }); + } + + const filePath = path.join(__dirname, '../public/uploads/products', filename); + + // Check if file exists + if (!fs.existsSync(filePath)) { + return res.status(404).json({ + error: true, + message: 'Image not found' + }); + } + + // Delete the file + fs.unlinkSync(filePath); + + res.json({ + success: true, + message: 'Image deleted successfully' + }); + } catch (error) { + console.error('Error deleting image:', error); + res.status(500).json({ + error: true, + message: 'Failed to delete image' + }); + } +}); + +// Use routes +app.use('/api/products', productRoutes(pool, query)); +app.use('/api/auth', authRoutes(pool, query)); +app.use('/api/cart', cartRoutes(pool, query, authMiddleware(pool, query))); +app.use('/api/admin/products', productAdminRoutes(pool, query, adminAuthMiddleware(pool, query))); + +app.use('/api/admin/categories', categoryAdminRoutes(pool, query, adminAuthMiddleware(pool, query))); // Add category admin routes +// Error handling middleware +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).json({ + error: true, + message: err.message || 'An unexpected error occurred' + }); +}); + +// Start server +app.listen(port, () => { + console.log(`Server running on port ${port} in ${config.environment} environment`); +}); + +module.exports = app; \ No newline at end of file diff --git a/backend/src/middleware/adminAuth.js b/backend/src/middleware/adminAuth.js new file mode 100644 index 0000000..52db624 --- /dev/null +++ b/backend/src/middleware/adminAuth.js @@ -0,0 +1,50 @@ +/** + * Admin authentication middleware to protect admin routes + */ +module.exports = (pool, query) => { + return async (req, res, next) => { + // Get API key from header + const apiKey = req.headers['x-api-key']; + + if (!apiKey) { + return res.status(401).json({ + error: true, + message: 'API key is required' + }); + } + + try { + // Verify API key and check admin status + const result = await query( + 'SELECT id, email, first_name, last_name, is_admin FROM users WHERE api_key = $1', + [apiKey] + ); + + if (result.rows.length === 0) { + return res.status(401).json({ + error: true, + message: 'Invalid API key' + }); + } + + // Check if user is admin + if (!result.rows[0].is_admin) { + return res.status(403).json({ + error: true, + message: 'Admin access required' + }); + } + + // Add user to request object + req.user = result.rows[0]; + + // Continue to next middleware/route handler + next(); + } catch (error) { + return res.status(500).json({ + error: true, + message: 'Error authenticating user' + }); + } + }; + }; \ No newline at end of file diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js new file mode 100644 index 0000000..a009b32 --- /dev/null +++ b/backend/src/middleware/auth.js @@ -0,0 +1,42 @@ +/** + * Authentication middleware to protect routes + */ +module.exports = (pool, query) => { + return async (req, res, next) => { + // Get API key from header + const apiKey = req.headers['x-api-key']; + + if (!apiKey) { + return res.status(401).json({ + error: true, + message: 'API key is required' + }); + } + + try { + // Verify API key + const result = await query( + 'SELECT id, email, first_name, last_name, is_admin FROM users WHERE api_key = $1', + [apiKey] + ); + + if (result.rows.length === 0) { + return res.status(401).json({ + error: true, + message: 'Invalid API key' + }); + } + + // Add user to request object + req.user = result.rows[0]; + + // Continue to next middleware/route handler + next(); + } catch (error) { + return res.status(500).json({ + error: true, + message: 'Error authenticating user' + }); + } + }; + }; \ No newline at end of file diff --git a/backend/src/middleware/upload.js b/backend/src/middleware/upload.js new file mode 100644 index 0000000..e2b5087 --- /dev/null +++ b/backend/src/middleware/upload.js @@ -0,0 +1,59 @@ +const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); +const { v4: uuidv4 } = require('uuid'); + +// Create uploads directory if it doesn't exist +const uploadDir = path.join(__dirname, '../../public/uploads'); +if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); +} + +// Create directories for different image types +const productImagesDir = path.join(uploadDir, 'products'); +if (!fs.existsSync(productImagesDir)) { + fs.mkdirSync(productImagesDir, { recursive: true }); +} + +// Configure storage +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + // Determine destination based on upload type + if (req.path.includes('/products')) { + cb(null, productImagesDir); + } else { + cb(null, uploadDir); + } + }, + filename: (req, file, cb) => { + // Create unique filename with original extension + const uniqueId = uuidv4(); + const fileExt = path.extname(file.originalname); + const safeName = path.basename(file.originalname, fileExt) + .toLowerCase() + .replace(/[^a-z0-9]/g, '-'); + + cb(null, `${safeName}-${uniqueId}${fileExt}`); + } +}); + +// File filter to only allow images +const fileFilter = (req, file, cb) => { + // Accept only image files + if (file.mimetype.startsWith('image/')) { + cb(null, true); + } else { + cb(new Error('Only image files are allowed!'), false); + } +}; + +// Create the multer instance +const upload = multer({ + storage, + fileFilter, + limits: { + fileSize: 5 * 1024 * 1024 // 5MB limit + } +}); + +module.exports = upload; \ No newline at end of file diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js new file mode 100644 index 0000000..57a8213 --- /dev/null +++ b/backend/src/routes/auth.js @@ -0,0 +1,271 @@ +const express = require('express'); +const { v4: uuidv4 } = require('uuid'); +const nodemailer = require('nodemailer'); +const config = require('../config'); + +const router = express.Router(); +const createTransporter = () => { + return nodemailer.createTransport({ + host: config.email.host, + port: config.email.port, + auth: { + user: config.email.user, + pass: config.email.pass + } + }); +}; + +// Mock email transporter for development +const transporter = createTransporter(); + +module.exports = (pool, query) => { + // Register new user + router.post('/register', async (req, res, next) => { + const { email, firstName, lastName } = req.body; + + try { + // Check if user already exists + const userCheck = await query( + 'SELECT * FROM users WHERE email = $1', + [email] + ); + + if (userCheck.rows.length > 0) { + return res.status(400).json({ + error: true, + message: 'User with this email already exists' + }); + } + + // Create new user + const result = await query( + 'INSERT INTO users (email, first_name, last_name) VALUES ($1, $2, $3) RETURNING id, email', + [email, firstName, lastName] + ); + + res.status(201).json({ + message: 'User registered successfully', + user: result.rows[0] + }); + } catch (error) { + next(error); + } + }); + + // Request login code + router.post('/login-request', async (req, res, next) => { + const { email } = req.body; + + try { + // Check if user exists + const userResult = await query( + 'SELECT * FROM users WHERE email = $1', + [email] + ); + + if (userResult.rows.length === 0) { + return res.status(404).json({ + error: true, + message: 'User not found' + }); + } + + const user = userResult.rows[0]; + + // Generate a random 6-digit code + // Set expiration time (15 minutes from now) + const authCode = Math.floor(100000 + Math.random() * 900000).toString(); + + const expiresAt = new Date(); + expiresAt.setMinutes(expiresAt.getMinutes() + 15); + + // Create auth record + const authResult = await query( + 'INSERT INTO authentications (code, expires_at) VALUES ($1, $2) RETURNING id', + [authCode, expiresAt] + ); + + const authId = authResult.rows[0].id; + + // Update user with auth record + await query( + 'UPDATE users SET current_auth_id = $1 WHERE id = $2', + [authId, user.id] + ); + + // Send email with code (mock in development) + const loginLink = `${config.site.protocol}://${config.site.domain}/verify?code=${authCode}&email=${encodeURIComponent(email)}`; + + + + await transporter.sendMail({ + from: 'noreply@2many.ca', + to: email, + subject: 'Your Login Code', + html: ` +

Your login code is: ${authCode}

+

This code will expire in 15 minutes.

+

Or click here to log in directly.

+ ` + }); + let retObj = { + message: 'Login code sent to address: ' + email + } + + if(process.env.ENVIRONMENT === "beta"){ + retObj.code = authCode; + } + + res.json(retObj); + } catch (error) { + next(error); + } + }); + + // Verify login code + router.post('/verify', async (req, res, next) => { + const { email, code } = req.body; + + try { + // Get user and auth record + const userResult = await query( + `SELECT u.*, a.code, a.expires_at, a.is_used + FROM users u + JOIN authentications a ON u.current_auth_id = a.id + WHERE u.email = $1`, + [email] + ); + + if (userResult.rows.length === 0) { + return res.status(400).json({ + error: true, + message: 'Invalid request or no login code requested' + }); + } + + const { code: storedCode, expires_at, is_used, id: userId } = userResult.rows[0]; + + // Check code + if (storedCode !== code) { + return res.status(400).json({ + error: true, + message: 'Invalid code' + }); + } + + // Check if expired + if (new Date() > new Date(expires_at)) { + return res.status(400).json({ + error: true, + message: 'Code has expired, Please restart login' + }); + } + + // Check if already used + if (is_used) { + return res.status(400).json({ + error: true, + message: 'Code has already been used' , + data: is_used + }); + } + + // Mark auth as used + await query( + 'UPDATE authentications SET is_used = true WHERE code = $1', + [code] + ); + + // Generate API key + const apiKey = uuidv4(); + + // Save API key to user + await query( + 'UPDATE users SET api_key = $1, last_login = NOW() WHERE id = $2', + [apiKey, userId] + ); + + // Get user information including admin status + const userInfo = await query( + 'SELECT id, email, first_name, last_name, is_admin FROM users WHERE id = $1', + [userId] + ); + + res.json({ + message: 'Login successful', + userId: userId, + isAdmin: userInfo.rows[0].is_admin, + apiKey: apiKey + }); + } catch (error) { + next(error); + } + }); + //apikey check + router.post('/verify-key', async (req, res, next) => { + const { apiKey, email } = req.body; + + try { + // Verify the API key against email + const result = await query( + 'SELECT id, email, first_name, last_name, is_admin FROM users WHERE api_key = $1 AND email = $2', + [apiKey, email] + ); + + if (result.rows.length === 0) { + return res.status(401).json({ + error: true, + message: 'Invalid API key' + }); + } + + res.json({ + valid: true, + user: result.rows[0] + }); + } catch (error) { + next(error); + } + }); + // Logout + router.post('/logout', async (req, res, next) => { + const { userId } = req.body; + + try { + // Get current auth ID + const userResult = await query( + 'SELECT current_auth_id FROM users WHERE id = $1', + [userId] + ); + + if (userResult.rows.length === 0) { + return res.status(404).json({ + error: true, + message: 'User not found' + }); + } + + const authId = userResult.rows[0].current_auth_id; + + // Clear auth ID and API key + await query( + 'UPDATE users SET current_auth_id = NULL, api_key = NULL WHERE id = $1', + [userId] + ); + + // Delete auth record + if (authId) { + await query( + 'DELETE FROM authentications WHERE id = $1', + [authId] + ); + } + + res.json({ message: 'Logged out successfully' }); + } catch (error) { + next(error); + } + }); + + return router; +}; \ No newline at end of file diff --git a/backend/src/routes/cart.js b/backend/src/routes/cart.js new file mode 100644 index 0000000..97526fd --- /dev/null +++ b/backend/src/routes/cart.js @@ -0,0 +1,363 @@ +const express = require('express'); +const { v4: uuidv4 } = require('uuid'); +const router = express.Router(); + +module.exports = (pool, query, authMiddleware) => { + + router.use(authMiddleware); + + // Get user's cart + router.get('/:userId', async (req, res, next) => { + try { + const { userId } = req.params; + if (req.user.id !== userId) { + return res.status(403).json({ + error: true, + message: 'You can only access your own cart' + }); + } + // Get cart + let cartResult = await query( + 'SELECT * FROM carts WHERE user_id = $1', + [userId] + ); + + // If no cart exists, create one + if (cartResult.rows.length === 0) { + cartResult = await query( + 'INSERT INTO carts (id, user_id) VALUES ($1, $2) RETURNING *', + [uuidv4(), userId] + ); + } + + const cartId = cartResult.rows[0].id; + + // Get cart items with product details + const cartItemsResult = await query( + `SELECT ci.id, ci.quantity, ci.added_at, + p.id AS product_id, p.name, p.description, p.price, p.image_url, + p.category_id, pc.name AS category_name + FROM cart_items ci + JOIN products p ON ci.product_id = p.id + JOIN product_categories pc ON p.category_id = pc.id + WHERE ci.cart_id = $1`, + [cartId] + ); + + // Calculate total + const total = cartItemsResult.rows.reduce((sum, item) => { + + return sum + (parseFloat(item.price) * item.quantity); + }, 0); + + res.json({ + id: cartId, + userId, + items: cartItemsResult.rows, + itemCount: cartItemsResult.rows.length, + total + }); + } catch (error) { + next(error); + } + }); + + // Add item to cart + router.post('/add', async (req, res, next) => { + try { + const { userId, productId, quantity = 1 } = req.body; + if (req.user.id !== userId) { + return res.status(403).json({ + error: true, + message: 'You can only modify your own cart' + }); + } + // Check if product exists + const productResult = await query( + 'SELECT * FROM products WHERE id = $1', + [productId] + ); + + if (productResult.rows.length === 0) { + return res.status(404).json({ + error: true, + message: 'Product not found' + }); + } + + // Get or create cart + let cartResult = await query( + 'SELECT * FROM carts WHERE user_id = $1', + [userId] + ); + + if (cartResult.rows.length === 0) { + cartResult = await query( + 'INSERT INTO carts (id, user_id) VALUES ($1, $2) RETURNING *', + [uuidv4(), userId] + ); + } + + const cartId = cartResult.rows[0].id; + + // Check if item already in cart + const existingItemResult = await query( + 'SELECT * FROM cart_items WHERE cart_id = $1 AND product_id = $2', + [cartId, productId] + ); + + if (existingItemResult.rows.length > 0) { + // Update quantity + const newQuantity = existingItemResult.rows[0].quantity + quantity; + + await query( + 'UPDATE cart_items SET quantity = $1 WHERE id = $2', + [newQuantity, existingItemResult.rows[0].id] + ); + } else { + // Add new item + await query( + 'INSERT INTO cart_items (id, cart_id, product_id, quantity) VALUES ($1, $2, $3, $4)', + [uuidv4(), cartId, productId, quantity] + ); + } + + // Get updated cart + const updatedCartItems = await query( + `SELECT ci.id, ci.quantity, ci.added_at, + p.id AS product_id, p.name, p.description, p.price, p.image_url, + p.category_id, pc.name AS category_name + FROM cart_items ci + JOIN products p ON ci.product_id = p.id + JOIN product_categories pc ON p.category_id = pc.id + WHERE ci.cart_id = $1`, + [cartId] + ); + + // Calculate total + const total = updatedCartItems.rows.reduce((sum, item) => { + return sum + (parseFloat(item.price) * item.quantity); + }, 0); + + res.json({ + id: cartId, + userId, + items: updatedCartItems.rows, + itemCount: updatedCartItems.rows.length, + total + }); + } catch (error) { + next(error); + } + }); + + // Update cart item quantity + router.put('/update', async (req, res, next) => { + try { + const { userId, productId, quantity } = req.body; + if (req.user.id !== userId) { + return res.status(403).json({ + error: true, + message: 'You can only modify your own cart' + }); + } + // Get cart + const cartResult = await query( + 'SELECT * FROM carts WHERE user_id = $1', + [userId] + ); + + if (cartResult.rows.length === 0) { + return res.status(404).json({ + error: true, + message: 'Cart not found' + }); + } + + const cartId = cartResult.rows[0].id; + + if (quantity <= 0) { + // Remove item + await query( + 'DELETE FROM cart_items WHERE cart_id = $1 AND product_id = $2', + [cartId, productId] + ); + } else { + // Update quantity + await query( + 'UPDATE cart_items SET quantity = $1 WHERE cart_id = $2 AND product_id = $3', + [quantity, cartId, productId] + ); + } + + // Get updated cart + const updatedCartItems = await query( + `SELECT ci.id, ci.quantity, ci.added_at, + p.id AS product_id, p.name, p.description, p.price, p.image_url, + p.category_id, pc.name AS category_name + FROM cart_items ci + JOIN products p ON ci.product_id = p.id + JOIN product_categories pc ON p.category_id = pc.id + WHERE ci.cart_id = $1`, + [cartId] + ); + + // Calculate total + const total = updatedCartItems.rows.reduce((sum, item) => { + return sum + (parseFloat(item.price) * item.quantity); + }, 0); + + res.json({ + id: cartId, + userId, + items: updatedCartItems.rows, + itemCount: updatedCartItems.rows.length, + total + }); + } catch (error) { + next(error); + } + }); + + // Clear cart + router.delete('/clear/:userId', async (req, res, next) => { + try { + const { userId } = req.params; + if (req.user.id !== userId) { + return res.status(403).json({ + error: true, + message: 'You can only modify your own cart' + }); + } + // Get cart + const cartResult = await query( + 'SELECT * FROM carts WHERE user_id = $1', + [userId] + ); + + if (cartResult.rows.length === 0) { + return res.status(404).json({ + error: true, + message: 'Cart not found' + }); + } + + const cartId = cartResult.rows[0].id; + + // Delete all items + await query( + 'DELETE FROM cart_items WHERE cart_id = $1', + [cartId] + ); + + res.json({ + id: cartId, + userId, + items: [], + itemCount: 0, + total: 0 + }); + } catch (error) { + next(error); + } + }); + + // Checkout (create order from cart) + router.post('/checkout', async (req, res, next) => { + try { + const { userId, shippingAddress } = req.body; + if (req.user.id !== userId) { + return res.status(403).json({ + error: true, + message: 'You can only checkout your own cart' + }); + } + // Get cart + const cartResult = await query( + 'SELECT * FROM carts WHERE user_id = $1', + [userId] + ); + + if (cartResult.rows.length === 0) { + return res.status(404).json({ + error: true, + message: 'Cart not found' + }); + } + + const cartId = cartResult.rows[0].id; + + // Get cart items + const cartItemsResult = await query( + `SELECT ci.*, p.price + FROM cart_items ci + JOIN products p ON ci.product_id = p.id + WHERE ci.cart_id = $1`, + [cartId] + ); + + if (cartItemsResult.rows.length === 0) { + return res.status(400).json({ + error: true, + message: 'Cart is empty' + }); + } + + // Calculate total + const total = cartItemsResult.rows.reduce((sum, item) => { + return sum + (parseFloat(item.price) * item.quantity); + }, 0); + + // Begin transaction + const client = await pool.connect(); + + try { + await client.query('BEGIN'); + + // Create order + const orderId = uuidv4(); + await client.query( + 'INSERT INTO orders (id, user_id, status, total_amount, shipping_address) VALUES ($1, $2, $3, $4, $5)', + [orderId, userId, 'pending', total, shippingAddress] + ); + + // Create order items + for (const item of cartItemsResult.rows) { + await client.query( + 'INSERT INTO order_items (id, order_id, product_id, quantity, price_at_purchase) VALUES ($1, $2, $3, $4, $5)', + [uuidv4(), orderId, item.product_id, item.quantity, item.price] + ); + + // Update product stock + await client.query( + 'UPDATE products SET stock_quantity = stock_quantity - $1 WHERE id = $2', + [item.quantity, item.product_id] + ); + } + + // Clear cart + await client.query( + 'DELETE FROM cart_items WHERE cart_id = $1', + [cartId] + ); + + await client.query('COMMIT'); + + res.status(201).json({ + success: true, + message: 'Order created successfully', + orderId + }); + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } + } catch (error) { + next(error); + } + }); + + return router; +}; \ No newline at end of file diff --git a/backend/src/routes/categoryAdmin.js b/backend/src/routes/categoryAdmin.js new file mode 100644 index 0000000..035fb64 --- /dev/null +++ b/backend/src/routes/categoryAdmin.js @@ -0,0 +1,186 @@ +const express = require('express'); +const { v4: uuidv4 } = require('uuid'); +const router = express.Router(); + +module.exports = (pool, query, authMiddleware) => { + // Apply authentication middleware to all routes + router.use(authMiddleware); + + // Get all categories + router.get('/', async (req, res, next) => { + try { + const result = await query('SELECT * FROM product_categories ORDER BY name ASC'); + res.json(result.rows); + } catch (error) { + next(error); + } + }); + + // Get single category by ID + router.get('/:id', async (req, res, next) => { + try { + const { id } = req.params; + + const result = await query( + 'SELECT * FROM product_categories WHERE id = $1', + [id] + ); + + if (result.rows.length === 0) { + return res.status(404).json({ + error: true, + message: 'Category not found' + }); + } + + res.json(result.rows[0]); + } catch (error) { + next(error); + } + }); + + // Create a new category + router.post('/', async (req, res, next) => { + try { + const { name, description, imagePath } = req.body; + + // Validate required fields + if (!name) { + return res.status(400).json({ + error: true, + message: 'Category name is required' + }); + } + + // Check if category with same name already exists + const existingCategory = await query( + 'SELECT * FROM product_categories WHERE name = $1', + [name] + ); + + if (existingCategory.rows.length > 0) { + return res.status(400).json({ + error: true, + message: 'A category with this name already exists' + }); + } + + // Create new category + const result = await query( + 'INSERT INTO product_categories (id, name, description, image_path) VALUES ($1, $2, $3, $4) RETURNING *', + [uuidv4(), name, description || null, imagePath || null] + ); + + res.status(201).json({ + message: 'Category created successfully', + category: result.rows[0] + }); + } catch (error) { + next(error); + } + }); + + // Update a category + router.put('/:id', async (req, res, next) => { + try { + const { id } = req.params; + const { name, description, imagePath } = req.body; + + // Validate required fields + if (!name) { + return res.status(400).json({ + error: true, + message: 'Category name is required' + }); + } + + // Check if category exists + const categoryCheck = await query( + 'SELECT * FROM product_categories WHERE id = $1', + [id] + ); + + if (categoryCheck.rows.length === 0) { + return res.status(404).json({ + error: true, + message: 'Category not found' + }); + } + + // Check if new name conflicts with existing category + if (name !== categoryCheck.rows[0].name) { + const nameCheck = await query( + 'SELECT * FROM product_categories WHERE name = $1 AND id != $2', + [name, id] + ); + + if (nameCheck.rows.length > 0) { + return res.status(400).json({ + error: true, + message: 'A category with this name already exists' + }); + } + } + + // Update category + const result = await query( + 'UPDATE product_categories SET name = $1, description = $2, image_path = $3 WHERE id = $4 RETURNING *', + [name, description || null, imagePath, id] + ); + + res.json({ + message: 'Category updated successfully', + category: result.rows[0] + }); + } catch (error) { + next(error); + } + }); + + // Delete a category + router.delete('/:id', async (req, res, next) => { + try { + const { id } = req.params; + + // Check if category exists + const categoryCheck = await query( + 'SELECT * FROM product_categories WHERE id = $1', + [id] + ); + + if (categoryCheck.rows.length === 0) { + return res.status(404).json({ + error: true, + message: 'Category not found' + }); + } + + // Check if category is being used by products + const productsUsingCategory = await query( + 'SELECT COUNT(*) FROM products WHERE category_id = $1', + [id] + ); + + if (parseInt(productsUsingCategory.rows[0].count) > 0) { + return res.status(400).json({ + error: true, + message: 'This category cannot be deleted because it is associated with products. Please reassign those products to a different category first.' + }); + } + + // Delete category + await query( + 'DELETE FROM product_categories WHERE id = $1', + [id] + ); + + res.json({ + message: 'Category deleted successfully' + }); + } catch (error) { + next(error); + } + }); + + return router; +}; \ No newline at end of file diff --git a/backend/src/routes/images.js b/backend/src/routes/images.js new file mode 100644 index 0000000..60975ec --- /dev/null +++ b/backend/src/routes/images.js @@ -0,0 +1,97 @@ +const express = require('express'); +const router = express.Router(); +const upload = require('../middleware/upload'); +const path = require('path'); +const fs = require('fs'); +const authMiddleware = require('../middleware/auth'); +const adminAuthMiddleware = require('../middleware/adminAuth'); + +module.exports = (pool, query) => { + // Apply authentication for all admin upload routes + router.use('/admin', adminAuthMiddleware(pool, query)); + + // Upload product image (admin only) + router.post('/admin/products', upload.single('image'), async (req, res, next) => { + try { + if (!req.file) { + return res.status(400).json({ + error: true, + message: 'No image file provided' + }); + } + + // Get the relative path to the image + const imagePath = `/uploads/products/${req.file.filename}`; + + res.json({ + success: true, + imagePath, + filename: req.file.filename + }); + } catch (error) { + next(error); + } + }); + + // Upload multiple product images (admin only) + router.post('/admin/products/multiple', upload.array('images', 10), async (req, res, next) => { + try { + if (!req.files || req.files.length === 0) { + return res.status(400).json({ + error: true, + message: 'No image files provided' + }); + } + + // Get the relative paths to the images + const imagePaths = req.files.map(file => ({ + imagePath: `/uploads/products/${file.filename}`, + filename: file.filename + })); + + res.json({ + success: true, + images: imagePaths + }); + } catch (error) { + next(error); + } + }); + + // Delete product image (admin only) + router.delete('/admin/products/:filename', async (req, res, next) => { + try { + const { filename } = req.params; + + // Prevent path traversal attacks + if (filename.includes('..') || filename.includes('/')) { + return res.status(400).json({ + error: true, + message: 'Invalid filename' + }); + } + + const filePath = path.join(__dirname, '../../public/uploads/products', filename); + + // Check if file exists + if (!fs.existsSync(filePath)) { + return res.status(404).json({ + error: true, + message: 'Image not found' + }); + } + + // Delete the file + fs.unlinkSync(filePath); + + res.json({ + success: true, + message: 'Image deleted successfully' + }); + } catch (error) { + next(error); + } + }); + + return router; +}; \ No newline at end of file diff --git a/backend/src/routes/productAdmin.js b/backend/src/routes/productAdmin.js new file mode 100644 index 0000000..7886803 --- /dev/null +++ b/backend/src/routes/productAdmin.js @@ -0,0 +1,437 @@ +const express = require('express'); +const { v4: uuidv4 } = require('uuid'); +const router = express.Router(); + +module.exports = (pool, query, authMiddleware) => { + // Apply authentication middleware to all routes + router.use(authMiddleware); + + // Create a new product with multiple images + router.post('/', async (req, res, next) => { + try { + const { + name, + description, + categoryName, + price, + stockQuantity, + weightGrams, + lengthCm, + widthCm, + heightCm, + origin, + age, + materialType, + color, + images, + tags + } = req.body; + + // Validate required fields + if (!name || !description || !categoryName || !price || !stockQuantity) { + return res.status(400).json({ + error: true, + message: 'Required fields missing: name, description, categoryName, price, and stockQuantity are mandatory' + }); + } + + // Begin transaction + const client = await pool.connect(); + + try { + await client.query('BEGIN'); + + // Get category ID by name + const categoryResult = await client.query( + 'SELECT id FROM product_categories WHERE name = $1', + [categoryName] + ); + + if (categoryResult.rows.length === 0) { + return res.status(404).json({ + error: true, + message: `Category "${categoryName}" not found` + }); + } + + const categoryId = categoryResult.rows[0].id; + + // Create product + const productId = uuidv4(); + await client.query( + `INSERT INTO products ( + id, name, description, category_id, price, stock_quantity, + weight_grams, length_cm, width_cm, height_cm, + origin, age, material_type, color + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)`, + [ + productId, name, description, categoryId, price, stockQuantity, + weightGrams || null, lengthCm || null, widthCm || null, heightCm || null, + origin || null, age || null, materialType || null, color || null + ] + ); + + // Add images if provided + if (images && images.length > 0) { + for (let i = 0; i < images.length; i++) { + const { path, isPrimary = (i === 0) } = images[i]; + + await client.query( + 'INSERT INTO product_images (product_id, image_path, display_order, is_primary) VALUES ($1, $2, $3, $4)', + [productId, path, i, isPrimary] + ); + } + } + + // Add tags if provided + if (tags && tags.length > 0) { + for (const tagName of tags) { + // Get tag ID + let tagResult = await client.query( + 'SELECT id FROM tags WHERE name = $1', + [tagName] + ); + + let tagId; + + // If tag doesn't exist, create it + if (tagResult.rows.length === 0) { + const newTagResult = await client.query( + 'INSERT INTO tags (name) VALUES ($1) RETURNING id', + [tagName] + ); + tagId = newTagResult.rows[0].id; + } else { + tagId = tagResult.rows[0].id; + } + + // Add tag to product + await client.query( + 'INSERT INTO product_tags (product_id, tag_id) VALUES ($1, $2)', + [productId, tagId] + ); + } + } + + await client.query('COMMIT'); + + // Get complete product with images and tags + const productQuery = ` + SELECT p.*, + pc.name as category_name, + ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL) AS tags, + json_agg(json_build_object( + 'id', pi.id, + 'path', pi.image_path, + 'isPrimary', pi.is_primary, + 'displayOrder', pi.display_order + )) FILTER (WHERE pi.id IS NOT NULL) AS images + FROM products p + JOIN product_categories pc ON p.category_id = pc.id + LEFT JOIN product_tags pt ON p.id = pt.product_id + LEFT JOIN tags t ON pt.tag_id = t.id + LEFT JOIN product_images pi ON p.id = pi.product_id + WHERE p.id = $1 + GROUP BY p.id, pc.name + `; + + const product = await query(productQuery, [productId]); + + res.status(201).json({ + message: 'Product created successfully', + product: product.rows[0] + }); + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } + } catch (error) { + next(error); + } + }); + + // Update an existing product + router.put('/:id', async (req, res, next) => { + try { + const { id } = req.params; + const { + name, + description, + categoryName, + price, + stockQuantity, + weightGrams, + lengthCm, + widthCm, + heightCm, + origin, + age, + materialType, + color, + images, + tags + } = req.body; + + // Begin transaction + const client = await pool.connect(); + + try { + await client.query('BEGIN'); + + // Check if product exists + const productCheck = await client.query( + 'SELECT * FROM products WHERE id = $1', + [id] + ); + + if (productCheck.rows.length === 0) { + return res.status(404).json({ + error: true, + message: 'Product not found' + }); + } + + // If category is changing, get the new category ID + let categoryId = productCheck.rows[0].category_id; + if (categoryName) { + const categoryResult = await client.query( + 'SELECT id FROM product_categories WHERE name = $1', + [categoryName] + ); + + if (categoryResult.rows.length === 0) { + return res.status(404).json({ + error: true, + message: `Category "${categoryName}" not found` + }); + } + + categoryId = categoryResult.rows[0].id; + } + + // Update product + const updateFields = []; + const updateValues = []; + let valueIndex = 1; + + if (name) { + updateFields.push(`name = $${valueIndex}`); + updateValues.push(name); + valueIndex++; + } + + if (description) { + updateFields.push(`description = $${valueIndex}`); + updateValues.push(description); + valueIndex++; + } + + if (categoryName) { + updateFields.push(`category_id = $${valueIndex}`); + updateValues.push(categoryId); + valueIndex++; + } + + if (price) { + updateFields.push(`price = $${valueIndex}`); + updateValues.push(price); + valueIndex++; + } + + if (stockQuantity !== undefined) { + updateFields.push(`stock_quantity = $${valueIndex}`); + updateValues.push(stockQuantity); + valueIndex++; + } + + if (weightGrams !== undefined) { + updateFields.push(`weight_grams = $${valueIndex}`); + updateValues.push(weightGrams); + valueIndex++; + } + + if (lengthCm !== undefined) { + updateFields.push(`length_cm = $${valueIndex}`); + updateValues.push(lengthCm); + valueIndex++; + } + + if (widthCm !== undefined) { + updateFields.push(`width_cm = $${valueIndex}`); + updateValues.push(widthCm); + valueIndex++; + } + + if (heightCm !== undefined) { + updateFields.push(`height_cm = $${valueIndex}`); + updateValues.push(heightCm); + valueIndex++; + } + + if (origin !== undefined) { + updateFields.push(`origin = $${valueIndex}`); + updateValues.push(origin); + valueIndex++; + } + + if (age !== undefined) { + updateFields.push(`age = $${valueIndex}`); + updateValues.push(age); + valueIndex++; + } + + if (materialType !== undefined) { + updateFields.push(`material_type = $${valueIndex}`); + updateValues.push(materialType); + valueIndex++; + } + + if (color !== undefined) { + updateFields.push(`color = $${valueIndex}`); + updateValues.push(color); + valueIndex++; + } + + if (updateFields.length > 0) { + const updateQuery = ` + UPDATE products + SET ${updateFields.join(', ')} + WHERE id = $${valueIndex} + `; + + updateValues.push(id); + + await client.query(updateQuery, updateValues); + } + + // Update images if provided + if (images) { + // Remove existing images + await client.query( + 'DELETE FROM product_images WHERE product_id = $1', + [id] + ); + + // Add new images + for (let i = 0; i < images.length; i++) { + const { path, isPrimary = (i === 0) } = images[i]; + + await client.query( + 'INSERT INTO product_images (product_id, image_path, display_order, is_primary) VALUES ($1, $2, $3, $4)', + [id, path, i, isPrimary] + ); + } + } + + // Update tags if provided + if (tags) { + // Remove existing tags + await client.query( + 'DELETE FROM product_tags WHERE product_id = $1', + [id] + ); + + // Add new tags + for (const tagName of tags) { + // Get tag ID + let tagResult = await client.query( + 'SELECT id FROM tags WHERE name = $1', + [tagName] + ); + + let tagId; + + // If tag doesn't exist, create it + if (tagResult.rows.length === 0) { + const newTagResult = await client.query( + 'INSERT INTO tags (name) VALUES ($1) RETURNING id', + [tagName] + ); + tagId = newTagResult.rows[0].id; + } else { + tagId = tagResult.rows[0].id; + } + + // Add tag to product + await client.query( + 'INSERT INTO product_tags (product_id, tag_id) VALUES ($1, $2)', + [id, tagId] + ); + } + } + + await client.query('COMMIT'); + + // Get updated product + const productQuery = ` + SELECT p.*, + pc.name as category_name, + ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL) AS tags, + json_agg(json_build_object( + 'id', pi.id, + 'path', pi.image_path, + 'isPrimary', pi.is_primary, + 'displayOrder', pi.display_order + )) FILTER (WHERE pi.id IS NOT NULL) AS images + FROM products p + JOIN product_categories pc ON p.category_id = pc.id + LEFT JOIN product_tags pt ON p.id = pt.product_id + LEFT JOIN tags t ON pt.tag_id = t.id + LEFT JOIN product_images pi ON p.id = pi.product_id + WHERE p.id = $1 + GROUP BY p.id, pc.name + `; + + const product = await query(productQuery, [id]); + + res.json({ + message: 'Product updated successfully', + product: product.rows[0] + }); + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } + } catch (error) { + next(error); + } + }); + + // Delete a product + router.delete('/:id', async (req, res, next) => { + try { + const { id } = req.params; + + // Check if product exists + const productCheck = await query( + 'SELECT * FROM products WHERE id = $1', + [id] + ); + + if (productCheck.rows.length === 0) { + return res.status(404).json({ + error: true, + message: 'Product not found' + }); + } + + // Delete product (cascade will handle related records) + await query( + 'DELETE FROM products WHERE id = $1', + [id] + ); + + res.json({ + message: 'Product deleted successfully' + }); + } catch (error) { + next(error); + } + }); + + return router; +}; \ No newline at end of file diff --git a/backend/src/routes/productAdminImages.js b/backend/src/routes/productAdminImages.js new file mode 100644 index 0000000..b43a8c0 --- /dev/null +++ b/backend/src/routes/productAdminImages.js @@ -0,0 +1,41 @@ +/** + * Process image paths and prepare them for database storage + * @param {Array} images - Array of image objects with paths + * @returns {Array} - Processed image objects with proper paths + */ +const processImagePaths = (images) => { + if (!images || !Array.isArray(images)) { + return []; + } + + return images.map((image, index) => { + // If the image is already a string (existing path), use it as is + if (typeof image === 'string') { + return { + path: image, + isPrimary: index === 0, // First image is primary by default + displayOrder: index + }; + } + + // If the image is an object from the upload middleware + if (image.path || image.imagePath) { + return { + path: image.imagePath || image.path, + isPrimary: image.isPrimary || index === 0, + displayOrder: image.displayOrder || index + }; + } + + // If the image is from the frontend (already has path field) + return { + path: image.path, + isPrimary: image.isPrimary || index === 0, + displayOrder: image.displayOrder || index + }; + }); + }; + + module.exports = { + processImagePaths + }; \ No newline at end of file diff --git a/backend/src/routes/products.js b/backend/src/routes/products.js new file mode 100644 index 0000000..d5b118b --- /dev/null +++ b/backend/src/routes/products.js @@ -0,0 +1,177 @@ +const express = require('express'); +const router = express.Router(); + +module.exports = (pool, qry) => { + // Get all products + router.get('/', async (req, res, next) => { + try { + const { category, tag, search, sort, order } = req.query; + + let query = ` + SELECT p.*, pc.name as category_name, + ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL) AS tags, + json_agg( + json_build_object( + 'id', pi.id, + 'path', pi.image_path, + 'isPrimary', pi.is_primary, + 'displayOrder', pi.display_order + ) ORDER BY pi.display_order + ) FILTER (WHERE pi.id IS NOT NULL) AS images + FROM products p + JOIN product_categories pc ON p.category_id = pc.id + LEFT JOIN product_tags pt ON p.id = pt.product_id + LEFT JOIN tags t ON pt.tag_id = t.id + LEFT JOIN product_images pi ON p.id = pi.product_id + `; + + const whereConditions = []; + const params = []; + let paramIndex = 1; + + // Filter by category + if (category) { + params.push(category); + whereConditions.push(`pc.name = $${params.length}`); + paramIndex++; + } + + // Filter by tag + if (tag) { + params.push(tag); + whereConditions.push(`t.name = $${params.length}`); + paramIndex++; + } + + // Search functionality + if (search) { + params.push(`%${search}%`); + whereConditions.push(`(p.name ILIKE $${params.length} OR p.description ILIKE $${params.length})`); + paramIndex++; + } + + // Add WHERE clause if we have conditions + if (whereConditions.length > 0) { + query += ` WHERE ${whereConditions.join(' AND ')}`; + } + + // Group by product id + query += ` GROUP BY p.id, pc.name`; + + // Sorting + if (sort) { + const validSortColumns = ['name', 'price', 'created_at']; + const validOrderDirections = ['asc', 'desc']; + + const sortColumn = validSortColumns.includes(sort) ? sort : 'name'; + const orderDirection = validOrderDirections.includes(order) ? order : 'asc'; + + query += ` ORDER BY p.${sortColumn} ${orderDirection}`; + } else { + // Default sort + query += ` ORDER BY p.name ASC`; + } + + const result = await qry(query, params); + + res.json(result.rows); + } catch (error) { + next(error); + } + }); + + // Get single product by ID + router.get('/:id', async (req, res, next) => { + try { + const { id } = req.params; + + const query = ` + SELECT p.*, pc.name as category_name, + ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL) AS tags, + json_agg( + json_build_object( + 'id', pi.id, + 'path', pi.image_path, + 'isPrimary', pi.is_primary, + 'displayOrder', pi.display_order + ) ORDER BY pi.display_order + ) FILTER (WHERE pi.id IS NOT NULL) AS images + FROM products p + JOIN product_categories pc ON p.category_id = pc.id + LEFT JOIN product_tags pt ON p.id = pt.product_id + LEFT JOIN tags t ON pt.tag_id = t.id + LEFT JOIN product_images pi ON p.id = pi.product_id + WHERE p.id = $1 + GROUP BY p.id, pc.name + `; + + const result = await qry(query, [id]); + + if (result.rows.length === 0) { + return res.status(404).json({ + error: true, + message: 'Product not found' + }); + } + + res.json(result.rows[0]); + } catch (error) { + next(error); + } + }); + + // Get all product categories + router.get('/categories/all', async (req, res, next) => { + try { + const result = await qry('SELECT * FROM product_categories'); + res.json(result.rows); + } catch (error) { + next(error); + } + }); + + // Get all tags + router.get('/tags/all', async (req, res, next) => { + try { + const result = await qry('SELECT * FROM tags'); + res.json(result.rows); + } catch (error) { + next(error); + } + }); + + // Get products by category + router.get('/category/:categoryName', async (req, res, next) => { + try { + const { categoryName } = req.params; + + const query = ` + SELECT p.*, + ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL) AS tags, + json_agg( + json_build_object( + 'id', pi.id, + 'path', pi.image_path, + 'isPrimary', pi.is_primary, + 'displayOrder', pi.display_order + ) ORDER BY pi.display_order + ) FILTER (WHERE pi.id IS NOT NULL) AS images + FROM products p + JOIN product_categories pc ON p.category_id = pc.id + LEFT JOIN product_tags pt ON p.id = pt.product_id + LEFT JOIN tags t ON pt.tag_id = t.id + LEFT JOIN product_images pi ON p.id = pi.product_id + WHERE pc.name = $1 + GROUP BY p.id + `; + + const result = await qry(query, [categoryName]); + + res.json(result.rows); + } catch (error) { + next(error); + } + }); + + return router; +}; \ No newline at end of file diff --git a/db/init/01-schema.sql b/db/init/01-schema.sql new file mode 100644 index 0000000..39056fa --- /dev/null +++ b/db/init/01-schema.sql @@ -0,0 +1,170 @@ +-- Create UUID extension for generating UUIDs +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Create users table +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + email VARCHAR(255) UNIQUE NOT NULL, + first_name VARCHAR(100), + last_name VARCHAR(100), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + last_login TIMESTAMP WITH TIME ZONE, + current_auth_id UUID, -- Foreign key to authentication table, NULL if logged out + is_active BOOLEAN DEFAULT TRUE +); + +-- Create authentication table +CREATE TABLE authentications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + code VARCHAR(6) NOT NULL, -- 6-digit authentication code + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + expires_at TIMESTAMP WITH TIME ZONE NOT NULL, -- When this authentication code expires + is_used BOOLEAN DEFAULT FALSE -- Track if this code has been used +); + +-- Add foreign key constraint +ALTER TABLE users +ADD CONSTRAINT fk_user_authentication +FOREIGN KEY (current_auth_id) +REFERENCES authentications (id) +ON DELETE SET NULL; -- If auth record is deleted, just set NULL in users table + +-- Create product_categories table +CREATE TABLE product_categories ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(50) NOT NULL UNIQUE, + description TEXT +); + +-- Insert the three main product categories +INSERT INTO product_categories (name, description) VALUES + ('Rock', 'Natural stone specimens of various types, sizes, and origins'), + ('Bone', 'Preserved bones from various sources and species'), + ('Stick', 'Natural wooden sticks and branches of different types and sizes'); + +-- Create products table +CREATE TABLE products ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(255) NOT NULL, + description TEXT, + category_id UUID NOT NULL REFERENCES product_categories(id), + price DECIMAL(10, 2) NOT NULL, + stock_quantity INTEGER NOT NULL DEFAULT 0, + weight_grams DECIMAL(10, 2), + length_cm DECIMAL(10, 2), + width_cm DECIMAL(10, 2), + height_cm DECIMAL(10, 2), + origin VARCHAR(100), + age VARCHAR(100), + material_type VARCHAR(100), + color VARCHAR(100), + image_url VARCHAR(255), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE +); + +-- Create orders table +CREATE TABLE orders ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL, + status VARCHAR(50) NOT NULL DEFAULT 'pending', -- pending, processing, shipped, delivered, cancelled + total_amount DECIMAL(10, 2) NOT NULL, + shipping_address TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + FOREIGN KEY (user_id) REFERENCES users (id) +); + +-- Create order_items table for order details +CREATE TABLE order_items ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + order_id UUID NOT NULL, + product_id UUID NOT NULL, + quantity INTEGER NOT NULL, + price_at_purchase DECIMAL(10, 2) NOT NULL, -- Store price at time of purchase + FOREIGN KEY (order_id) REFERENCES orders (id) ON DELETE CASCADE, + FOREIGN KEY (product_id) REFERENCES products (id) +); + +-- Create shopping cart table +CREATE TABLE carts ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE +); + +-- Create cart items table +CREATE TABLE cart_items ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + cart_id UUID NOT NULL, + product_id UUID NOT NULL, + quantity INTEGER NOT NULL DEFAULT 1, + added_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + FOREIGN KEY (cart_id) REFERENCES carts (id) ON DELETE CASCADE, + FOREIGN KEY (product_id) REFERENCES products (id), + UNIQUE(cart_id, product_id) -- Prevent duplicate products in cart +); + +-- Create indexes for performance +CREATE INDEX idx_user_email ON users(email); +CREATE INDEX idx_auth_code ON authentications(code); +-- Create tags table +CREATE TABLE tags ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(50) NOT NULL UNIQUE, + description TEXT +); + +-- Create product_tags junction table +CREATE TABLE product_tags ( + product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE, + tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + PRIMARY KEY (product_id, tag_id) +); + +-- Insert common tags for natural items +INSERT INTO tags (name, description) VALUES + ('Polished', 'Items that have been polished to a smooth finish'), + ('Raw', 'Items in their natural, unprocessed state'), + ('Rare', 'Uncommon or hard-to-find specimens'), + ('Fossil', 'Preserved remains or traces of ancient organisms'), + ('Decorative', 'Items selected for their aesthetic appeal'), + ('Educational', 'Items with significant educational value'), + ('Collectible', 'Items that are part of a recognized collection series'); + +CREATE INDEX idx_product_name ON products(name); +CREATE INDEX idx_product_category ON products(category_id); +CREATE INDEX idx_product_tags_product ON product_tags(product_id); +CREATE INDEX idx_product_tags_tag ON product_tags(tag_id); +CREATE INDEX idx_orders_user_id ON orders(user_id); +CREATE INDEX idx_order_items_order_id ON order_items(order_id); + +-- Create a function to update the updated_at timestamp +CREATE OR REPLACE FUNCTION update_modified_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE 'plpgsql'; + +-- Create triggers to automatically update the updated_at column +CREATE TRIGGER update_users_modtime +BEFORE UPDATE ON users +FOR EACH ROW EXECUTE FUNCTION update_modified_column(); + +CREATE TRIGGER update_products_modtime +BEFORE UPDATE ON products +FOR EACH ROW EXECUTE FUNCTION update_modified_column(); + +CREATE TRIGGER update_orders_modtime +BEFORE UPDATE ON orders +FOR EACH ROW EXECUTE FUNCTION update_modified_column(); + +CREATE TRIGGER update_carts_modtime +BEFORE UPDATE ON carts +FOR EACH ROW EXECUTE FUNCTION update_modified_column(); \ No newline at end of file diff --git a/db/init/02-seed.sql b/db/init/02-seed.sql new file mode 100644 index 0000000..a39a2c3 --- /dev/null +++ b/db/init/02-seed.sql @@ -0,0 +1,191 @@ +-- Seed data for testing + +-- Insert test users +INSERT INTO users (email, first_name, last_name) VALUES + ('shaivkamat@2many.ca', 'Shaiv', 'Kamat'), + ('jane@example.com', 'Jane', 'Smith'), + ('bob@example.com', 'Bob', 'Johnson'); + +-- Note: Product categories are already inserted in the schema file, so we're skipping that step + +-- Insert rock products +WITH categories AS ( + SELECT id, name FROM product_categories +) +-- Insert rock products +INSERT INTO products (name, description, category_id, price, stock_quantity, weight_grams, color, material_type, origin, image_url) +SELECT + 'Amethyst Geode', + 'Beautiful purple amethyst geode with crystal formation', + id, + 49.99, + 15, + 450.0, + 'Purple', + 'Quartz', + 'Brazil', + '/images/amethyst-geode.jpg' +FROM categories WHERE name = 'Rock'; + +WITH categories AS ( + SELECT id, name FROM product_categories +) +INSERT INTO products (name, description, category_id, price, stock_quantity, weight_grams, color, material_type, origin, image_url) +SELECT + 'Polished Labradorite', + 'Stunning polished labradorite with iridescent blue flash', + id, + 29.99, + 25, + 120.5, + 'Gray/Blue', + 'Feldspar', + 'Madagascar', + '/images/labradorite.jpg' +FROM categories WHERE name = 'Rock'; + +WITH categories AS ( + SELECT id, name FROM product_categories +) +INSERT INTO products (name, description, category_id, price, stock_quantity, weight_grams, color, material_type, origin, image_url) +SELECT + 'Raw Turquoise', + 'Natural turquoise specimen, unpolished', + id, + 19.99, + 18, + 85.2, + 'Turquoise', + 'Turquoise', + 'Arizona', + '/images/turquoise.jpg' +FROM categories WHERE name = 'Rock'; + +-- Insert bone products +WITH categories AS ( + SELECT id, name FROM product_categories +) +INSERT INTO products (name, description, category_id, price, stock_quantity, length_cm, material_type, image_url) +SELECT + 'Deer Antler', + 'Naturally shed deer antler, perfect for display or crafts', + id, + 24.99, + 8, + 38.5, + 'Antler', + '/images/deer-antler.jpg' +FROM categories WHERE name = 'Bone'; + +WITH categories AS ( + SELECT id, name FROM product_categories +) +INSERT INTO products (name, description, category_id, price, stock_quantity, length_cm, material_type, image_url) +SELECT + 'Fossil Fish', + 'Well-preserved fossil fish from the Green River Formation', + id, + 89.99, + 5, + 22.8, + 'Fossilized Bone', + '/images/fossil-fish.jpg' +FROM categories WHERE name = 'Bone'; + +-- Insert stick products +WITH categories AS ( + SELECT id, name FROM product_categories +) +INSERT INTO products (name, description, category_id, price, stock_quantity, length_cm, width_cm, material_type, color, image_url) +SELECT + 'Driftwood Piece', + 'Unique driftwood piece, weathered by the ocean', + id, + 14.99, + 12, + 45.6, + 8.3, + 'Driftwood', + 'Tan/Gray', + '/images/driftwood.jpg' +FROM categories WHERE name = 'Stick'; + +WITH categories AS ( + SELECT id, name FROM product_categories +) +INSERT INTO products (name, description, category_id, price, stock_quantity, length_cm, width_cm, material_type, color, image_url) +SELECT + 'Walking Stick', + 'Hand-selected natural maple walking stick', + id, + 34.99, + 10, + 152.4, + 3.8, + 'Maple', + 'Brown', + '/images/walking-stick.jpg' +FROM categories WHERE name = 'Stick'; + +WITH categories AS ( + SELECT id, name FROM product_categories +) +INSERT INTO products (name, description, category_id, price, stock_quantity, length_cm, width_cm, material_type, color, image_url) +SELECT + 'Decorative Branch Set', + 'Set of 3 decorative birch branches for home decoration', + id, + 19.99, + 20, + 76.2, + 1.5, + 'Birch', + 'White', + '/images/birch-branches.jpg' +FROM categories WHERE name = 'Stick'; + +-- Create a cart for testing +INSERT INTO carts (user_id) +SELECT id FROM users WHERE email = 'jane@example.com'; + +-- Add product tags - using a different approach +-- Tag: Decorative for Amethyst Geode +INSERT INTO product_tags (product_id, tag_id) +SELECT p.id, t.id +FROM products p, tags t +WHERE p.name = 'Amethyst Geode' AND t.name = 'Decorative'; + +-- Tag: Polished for Polished Labradorite +INSERT INTO product_tags (product_id, tag_id) +SELECT p.id, t.id +FROM products p, tags t +WHERE p.name = 'Polished Labradorite' AND t.name = 'Polished'; + +-- Tag: Raw for Raw Turquoise +INSERT INTO product_tags (product_id, tag_id) +SELECT p.id, t.id +FROM products p, tags t +WHERE p.name = 'Raw Turquoise' AND t.name = 'Raw'; + +-- Tags: Fossil and Educational for Fossil Fish +INSERT INTO product_tags (product_id, tag_id) +SELECT p.id, t.id +FROM products p, tags t +WHERE p.name = 'Fossil Fish' AND t.name = 'Fossil'; + +INSERT INTO product_tags (product_id, tag_id) +SELECT p.id, t.id +FROM products p, tags t +WHERE p.name = 'Fossil Fish' AND t.name = 'Educational'; + +-- Tag: Decorative for Driftwood Piece +INSERT INTO product_tags (product_id, tag_id) +SELECT p.id, t.id +FROM products p, tags t +WHERE p.name = 'Driftwood Piece' AND t.name = 'Decorative'; + +-- Tag: Collectible for Walking Stick +INSERT INTO product_tags (product_id, tag_id) +SELECT p.id, t.id +FROM products p, tags t +WHERE p.name = 'Walking Stick' AND t.name = 'Collectible'; \ No newline at end of file diff --git a/db/init/03-api-key.sql b/db/init/03-api-key.sql new file mode 100644 index 0000000..22e0ceb --- /dev/null +++ b/db/init/03-api-key.sql @@ -0,0 +1,5 @@ +-- Add API key column to users table +ALTER TABLE users ADD COLUMN api_key VARCHAR(255) DEFAULT NULL; + +-- Create index for faster API key lookups +CREATE INDEX idx_user_api_key ON users(api_key); \ No newline at end of file diff --git a/db/init/04-product-images.sql b/db/init/04-product-images.sql new file mode 100644 index 0000000..47a626f --- /dev/null +++ b/db/init/04-product-images.sql @@ -0,0 +1,44 @@ +-- Create product_images table +CREATE TABLE product_images ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + product_id UUID NOT NULL, + image_path VARCHAR(255) NOT NULL, + display_order INT NOT NULL DEFAULT 0, + is_primary BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE +); + +-- Create indexes for performance +CREATE INDEX idx_product_images_product_id ON product_images(product_id); + +-- Modify existing products table to remove single image_url field +ALTER TABLE products DROP COLUMN IF EXISTS image_url; + +-- Insert test images for existing products +INSERT INTO product_images (product_id, image_path, display_order, is_primary) +SELECT id, '/images/amethyst-geode.jpg', 0, true FROM products WHERE name = 'Amethyst Geode'; + +INSERT INTO product_images (product_id, image_path, display_order, is_primary) +SELECT id, '/images/amethyst-geode-closeup.jpg', 1, false FROM products WHERE name = 'Amethyst Geode'; + +INSERT INTO product_images (product_id, image_path, display_order, is_primary) +SELECT id, '/images/labradorite.jpg', 0, true FROM products WHERE name = 'Polished Labradorite'; + +INSERT INTO product_images (product_id, image_path, display_order, is_primary) +SELECT id, '/images/turquoise.jpg', 0, true FROM products WHERE name = 'Raw Turquoise'; + +INSERT INTO product_images (product_id, image_path, display_order, is_primary) +SELECT id, '/images/deer-antler.jpg', 0, true FROM products WHERE name = 'Deer Antler'; + +INSERT INTO product_images (product_id, image_path, display_order, is_primary) +SELECT id, '/images/fossil-fish.jpg', 0, true FROM products WHERE name = 'Fossil Fish'; + +INSERT INTO product_images (product_id, image_path, display_order, is_primary) +SELECT id, '/images/driftwood.jpg', 0, true FROM products WHERE name = 'Driftwood Piece'; + +INSERT INTO product_images (product_id, image_path, display_order, is_primary) +SELECT id, '/images/walking-stick.jpg', 0, true FROM products WHERE name = 'Walking Stick'; + +INSERT INTO product_images (product_id, image_path, display_order, is_primary) +SELECT id, '/images/birch-branches.jpg', 0, true FROM products WHERE name = 'Decorative Branch Set'; \ No newline at end of file diff --git a/db/init/05-admin-role.sql b/db/init/05-admin-role.sql new file mode 100644 index 0000000..79cf191 --- /dev/null +++ b/db/init/05-admin-role.sql @@ -0,0 +1,10 @@ +-- Add is_admin column to users table +ALTER TABLE users ADD COLUMN is_admin BOOLEAN DEFAULT FALSE; + +-- Create index for faster admin lookups +CREATE INDEX idx_user_is_admin ON users(is_admin); + +-- Set the first user as admin for testing +UPDATE users +SET is_admin = TRUE +WHERE email = 'shaivkamat@2many.ca'; \ No newline at end of file diff --git a/db/init/06-product-categories.sql b/db/init/06-product-categories.sql new file mode 100644 index 0000000..56fea59 --- /dev/null +++ b/db/init/06-product-categories.sql @@ -0,0 +1 @@ +ALTER TABLE product_categories ADD COLUMN image_path VARCHAR(255); \ No newline at end of file diff --git a/db/test/test-api.sh b/db/test/test-api.sh new file mode 100644 index 0000000..cc48672 --- /dev/null +++ b/db/test/test-api.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +echo "Starting backend service..." +docker compose up -d db backend + +echo "Waiting for backend to start..." +sleep 5 + +echo "Testing API health endpoint..." +curl -s http://localhost:4000/health | jq + +echo "Fetching product categories..." +curl -s http://localhost:4000/api/products/categories/all | jq + +echo "Fetching all products..." +curl -s http://localhost:4000/api/products | jq + +echo "Fetching Rock category products..." +curl -s http://localhost:4000/api/products/category/Rock | jq + +echo "Testing user registration..." +curl -s -X POST http://localhost:4000/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{"email":"test@example.com","firstName":"Test","lastName":"User"}' | jq + +echo "Testing login request..." +curl -s -X POST http://localhost:4000/api/auth/login-request \ + -H "Content-Type: application/json" \ + -d '{"email":"test@example.com"}' | jq + +# Save the auth code for the next step +AUTH_CODE=$(curl -s -X POST http://localhost:4000/api/auth/login-request \ + -H "Content-Type: application/json" \ + -d '{"email":"test@example.com"}' | jq -r '.code') + +echo "Testing login verification with code: $AUTH_CODE" +RESPONSE=$(curl -s -X POST http://localhost:4000/api/auth/verify \ + -H "Content-Type: application/json" \ + -d "{\"email\":\"test@example.com\",\"code\":\"$AUTH_CODE\"}") + +echo $RESPONSE | jq + +USER_ID=$(echo $RESPONSE | jq -r '.userId') +API_KEY=$(echo $RESPONSE | jq -r '.token') + +echo "User ID: $USER_ID" +echo "API Key: $API_KEY" + +echo "Verifying API key..." +curl -s -X POST http://localhost:4000/api/auth/verify-key \ + -H "Content-Type: application/json" \ + -d "{\"apiKey\":\"$API_KEY\"}" | jq + +echo "Testing cart for user: $USER_ID with API key" +curl -s http://localhost:4000/api/cart/$USER_ID \ + -H "X-API-Key: $API_KEY" | jq + +echo "Adding a product to the cart..." +PRODUCT_ID=$(curl -s http://localhost:4000/api/products | jq -r '.[0].id') +curl -s -X POST http://localhost:4000/api/cart/add \ + -H "Content-Type: application/json" \ + -d "{\"userId\":\"$USER_ID\",\"productId\":\"$PRODUCT_ID\",\"quantity\":1}" | jq + +echo "Showing updated cart..." +curl -s http://localhost:4000/api/cart/$USER_ID | jq + +echo "Test complete!" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e891df0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,66 @@ +version: '3.8' + +services: + # Frontend React application + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - "3000:80" + volumes: + - ./frontend:/app + - /app/node_modules + depends_on: + - backend + environment: + - VITE_API_URL=http://localhost:4000/api + - VITE_ENVIRONMENT=beta + networks: + - app-network + + # Backend Express server + backend: + build: + context: ./backend + dockerfile: Dockerfile + env_file: + - ./backend/.env + ports: + - "4000:4000" + volumes: + - ./backend:/app + - /app/node_modules + - ./backend/public/uploads:/app/public/uploads # Persist uploads + depends_on: + db: + condition: service_healthy + networks: + - app-network + + # PostgreSQL Database + db: + image: postgres:14-alpine + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./db/init:/docker-entrypoint-initdb.d + env_file: + - ./backend/.env + restart: always + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + start_period: 10s + networks: + - app-network + +volumes: + postgres_data: + +networks: + app-network: + driver: bridge \ No newline at end of file diff --git a/fileStructure.txt b/fileStructure.txt new file mode 100644 index 0000000..b9272c4 --- /dev/null +++ b/fileStructure.txt @@ -0,0 +1,96 @@ +backend/ +├── public/ +├── node_modules/ +├── src/ +│ ├── routes/ +│ │ ├── auth.js +│ │ ├── products.js +│ │ ├── productAdminImages.js +│ │ ├── images.js +│ │ ├── productAdmin.js +│ │ └── cart.js +│ ├── middleware/ +│ │ ├── upload.js +│ │ ├── auth.js +│ │ └── adminAuth.js +│ ├── db/ +│ │ └── index.js +│ ├── index.js +│ └── config.js +│ └── Dockerfile +├── package.json +├── .env +├── README.md +└── .gitignore +db/ +├── init/ +│ ├── 05-admin-role.sql +│ ├── 02-seed.sql +│ ├── 04-product-images.sql +│ ├── 03-api-key.sql +│ ├── 01-schema.sql +frontend/ +├── node_modules/ +├── public/ +├── src/ +│ ├── pages/ +│ │ ├── Admin/ +│ │ │ ├── ProductEditPage.jsx +│ │ │ ├── DashboardPage.jsx +│ │ │ └── ProductsPage.jsx +│ │ ├── ProductsPage.jsx +│ │ ├── HomePage.jsx +│ │ ├── CartPage.jsx +│ │ ├── ProductDetailPage.jsx +│ │ ├── VerifyPage.jsx +│ │ ├── CheckoutPage.jsx +│ │ ├── RegisterPage.jsx +│ │ ├── NotFoundPage.jsx +│ │ └── LoginPage.jsx +│ ├── utils/ +│ │ └── imageUtils.js +│ ├── components/ +│ │ ├── ImageUploader.jsx +│ │ ├── ProductImage.jsx +│ │ ├── Footer.jsx +│ │ ├── ProtectedRoute.jsx +│ │ └── Notifications.jsx +│ ├── services/ +│ │ ├── imageService.js +│ │ ├── productService.js +│ │ ├── cartService.js +│ │ ├── authService.js +│ │ └── api.js +│ ├── hooks/ +│ │ ├── apiHooks.js +│ │ └── reduxHooks.js +│ ├── layouts/ +│ │ ├── MainLayout.jsx +│ │ ├── AuthLayout.jsx +│ │ └── AdminLayout.jsx +│ ├── theme/ +│ │ ├── index.js +│ │ └── ThemeProvider.jsx +│ ├── features/ +│ │ ├── ui/ +│ │ │ └── uiSlice.js +│ │ ├── cart/ +│ │ │ └── cartSlice.js +│ │ ├── auth/ +│ │ │ └── authSlice.js +│ │ └── store/ +│ │ └── index.js +│ ├── assets/ +│ ├── App.jsx +│ ├── main.jsx +│ └── config.js +├── package.json +├── package-lock.json +├── vite.config.js +├── Dockerfile +├── nginx.conf +├── index.html +├── README.md +├── .env +├── setup-frontend.sh +└── docker-compose.yml \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..b89de97 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,58 @@ +# FROM node:18-alpine as build + +# WORKDIR /app + +# # Copy package files first for better layer caching +# COPY package*.json ./ + +# # Use npm install instead of npm ci to resolve dependency mismatches +# RUN npm install + +# # Copy the rest of the application +# COPY . . + +# # Make sure we have a public directory for static assets +# RUN mkdir -p public + +# # Build the application +# RUN npm run build + +# # Production stage +# FROM nginx:alpine + +# # Copy built assets from the build stage +# COPY --from=build /app/dist /usr/share/nginx/html + +# # Copy custom nginx config if it exists, otherwise create a basic one +# # COPY nginx.conf /etc/nginx/conf.d/default.conf 2>/dev/null || echo 'server { \ +# # listen 80; \ +# # root /usr/share/nginx/html; \ +# # index index.html; \ +# # location / { \ +# # try_files $uri $uri/ /index.html; \ +# # } \ +# # location /api/ { \ +# # proxy_pass http://backend:4000; \ +# # } \ +# # }' > /etc/nginx/conf.d/default.conf + +# EXPOSE 80 + +# CMD ["nginx", "-g", "daemon off;"] + +# frontend/Dockerfile.dev +FROM node:18-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +# Don't copy source files - they will be mounted as a volume +# COPY . . + +# Expose the Vite dev server port +EXPOSE 3000 + +# Run the dev server +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..2d6fe96 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,66 @@ +# Rocks, Bones & Sticks Frontend + +React frontend for the Rocks, Bones & Sticks e-commerce platform. + +## Technologies Used + +- **React**: UI library +- **React Router**: For navigation and routing +- **Redux**: For state management +- **@reduxjs/toolkit**: Simplified Redux development +- **React Query**: For data fetching, caching, and state management +- **Material UI**: Component library with theming +- **Axios**: HTTP client +- **Vite**: Build tool + +## Project Structure + +``` +src/ +├── assets/ # Static assets like images, fonts +├── components/ # Reusable components +├── features/ # Redux slices organized by feature +├── hooks/ # Custom hooks +├── layouts/ # Layout components +├── pages/ # Page components +├── services/ # API services +├── store/ # Redux store configuration +├── theme/ # Material UI theme configuration +└── utils/ # Utility functions +``` + +## Setup + +```bash +# Install dependencies +npm install + +# Run for development +npm run dev + +# Build for production +npm run build + +# Preview production build +npm run preview +``` + +## Environment Variables + +Create a `.env` file with the following variables: + +``` +VITE_APP_NAME=Rocks, Bones & Sticks +VITE_API_URL=http://localhost:4000/api +VITE_ENVIRONMENT=beta # Use 'beta' for development, 'prod' for production +``` + +## Features + +- **User Authentication**: Login with email verification code +- **Product Browsing**: Filter and search products +- **Shopping Cart**: Add, update, remove items +- **Checkout Process**: Complete order creation +- **Admin Dashboard**: Manage products, view orders and customers +- **Responsive Design**: Works on mobile and desktop devices +- **Dark Mode Support**: Toggle between light and dark themes \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..8cb8ac9 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + Rocks, Bones & Sticks + + + +
+ + + \ No newline at end of file diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..1bd8c0a --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,37 @@ +server { + listen 80; + server_name localhost; + + # Document root + root /usr/share/nginx/html; + index index.html; + + # Handle SPA routing + location / { + try_files $uri $uri/ /index.html; + } + + # Proxy API requests to the backend + location /api/ { + proxy_pass http://backend:4000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } + + # Static asset caching + location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico)$ { + expires 30d; + add_header Cache-Control "public, no-transform"; + } + + # Enable gzip compression + gzip on; + gzip_vary on; + gzip_min_length 10240; + gzip_proxied expired no-cache no-store private auth; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml; + gzip_disable "MSIE [1-6]\."; +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..eeea975 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3664 @@ +{ + "name": "rocks-bones-sticks-frontend", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "requires": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + } + }, + "@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true + }, + "@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "dependencies": { + "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 + } + } + }, + "@babel/generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "requires": { + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", + "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "requires": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + } + }, + "@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true + }, + "@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==" + }, + "@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==" + }, + "@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", + "dev": true, + "requires": { + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" + } + }, + "@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "requires": { + "@babel/types": "^7.27.0" + } + }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", + "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz", + "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/runtime": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@babel/template": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "requires": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" + } + }, + "@babel/traverse": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "requires": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "requires": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + } + }, + "@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "requires": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "requires": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" + }, + "@emotion/is-prop-valid": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "requires": { + "@emotion/memoize": "^0.9.0" + } + }, + "@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, + "@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "requires": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + } + }, + "@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "requires": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" + }, + "@emotion/styled": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", + "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", + "requires": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + } + }, + "@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" + }, + "@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==" + }, + "@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==" + }, + "@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + }, + "@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "dev": true, + "optional": true + }, + "@eslint-community/eslint-utils": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", + "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.4.3" + } + }, + "@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.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" + }, + "dependencies": { + "globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + } + } + }, + "@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true + }, + "@fontsource/roboto": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.5.tgz", + "integrity": "sha512-70r2UZ0raqLn5W+sPeKhqlf8wGvUXFWlofaDlcbt/S3d06+17gXKr3VNqDODB0I1ASme3dGT5OJj9NABt7OTZQ==" + }, + "@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + } + }, + "@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 + }, + "@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@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==" + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@mui/core-downloads-tracker": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.17.1.tgz", + "integrity": "sha512-OcZj+cs6EfUD39IoPBOgN61zf1XFVY+imsGoBDwXeSq2UHJZE3N59zzBOVjclck91Ne3e9gudONOeILvHCIhUA==" + }, + "@mui/icons-material": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.17.1.tgz", + "integrity": "sha512-CN86LocjkunFGG0yPlO4bgqHkNGgaEOEc3X/jG5Bzm401qYw79/SaLrofA7yAKCCXAGdIGnLoMHohc3+ubs95A==", + "requires": { + "@babel/runtime": "^7.23.9" + } + }, + "@mui/material": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.17.1.tgz", + "integrity": "sha512-2B33kQf+GmPnrvXXweWAx+crbiUEsxCdCN979QDYnlH9ox4pd+0/IBriWLV+l6ORoBF60w39cWjFnJYGFdzXcw==", + "requires": { + "@babel/runtime": "^7.23.9", + "@mui/core-downloads-tracker": "^5.17.1", + "@mui/system": "^5.17.1", + "@mui/types": "~7.2.15", + "@mui/utils": "^5.17.1", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.0.0", + "react-transition-group": "^4.4.5" + }, + "dependencies": { + "react-is": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", + "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==" + } + } + }, + "@mui/private-theming": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.17.1.tgz", + "integrity": "sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ==", + "requires": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.17.1", + "prop-types": "^15.8.1" + } + }, + "@mui/styled-engine": { + "version": "5.16.14", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.14.tgz", + "integrity": "sha512-UAiMPZABZ7p8mUW4akDV6O7N3+4DatStpXMZwPlt+H/dA0lt67qawN021MNND+4QTpjaiMYxbhKZeQcyWCbuKw==", + "requires": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.13.5", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + } + }, + "@mui/system": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.17.1.tgz", + "integrity": "sha512-aJrmGfQpyF0U4D4xYwA6ueVtQcEMebET43CUmKMP7e7iFh3sMIF3sBR0l8Urb4pqx1CBjHAaWgB0ojpND4Q3Jg==", + "requires": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.17.1", + "@mui/styled-engine": "^5.16.14", + "@mui/types": "~7.2.15", + "@mui/utils": "^5.17.1", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + } + }, + "@mui/types": { + "version": "7.2.24", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", + "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==" + }, + "@mui/utils": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.17.1.tgz", + "integrity": "sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg==", + "requires": { + "@babel/runtime": "^7.23.9", + "@mui/types": "~7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.0.0" + }, + "dependencies": { + "react-is": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", + "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==" + } + } + }, + "@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, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@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 + }, + "@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, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "optional": true, + "requires": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1", + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + } + }, + "@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "dev": true, + "optional": true + }, + "@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "dev": true, + "optional": true + }, + "@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "dev": true, + "optional": true + }, + "@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "dev": true, + "optional": true + }, + "@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "dev": true, + "optional": true + }, + "@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "dev": true, + "optional": true + }, + "@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "dev": true, + "optional": true + }, + "@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" + }, + "@reduxjs/toolkit": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.7.0.tgz", + "integrity": "sha512-XVwolG6eTqwV0N8z/oDlN93ITCIGIop6leXlGJI/4EKy+0POYkR+ABHRSdGXY+0MQvJBP8yAzh+EYFxTuvmBiQ==", + "requires": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + } + }, + "@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==" + }, + "@rollup/rollup-android-arm-eabi": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", + "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz", + "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz", + "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz", + "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz", + "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz", + "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz", + "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz", + "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz", + "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz", + "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz", + "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz", + "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz", + "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz", + "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz", + "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", + "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz", + "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz", + "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz", + "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz", + "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", + "dev": true, + "optional": true + }, + "@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==" + }, + "@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==" + }, + "@tanstack/query-core": { + "version": "5.74.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.74.4.tgz", + "integrity": "sha512-YuG0A0+3i9b2Gfo9fkmNnkUWh5+5cFhWBN0pJAHkHilTx6A0nv8kepkk4T4GRt4e5ahbtFj2eTtkiPcVU1xO4A==" + }, + "@tanstack/query-devtools": { + "version": "5.74.6", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.74.6.tgz", + "integrity": "sha512-djaFT11mVCOW3e0Ezfyiq7T6OoHy2LRI1fUFQvj+G6+/4A1FkuRMNUhQkdP1GXlx8id0f1/zd5fgDpIy5SU/Iw==" + }, + "@tanstack/react-query": { + "version": "5.74.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.74.4.tgz", + "integrity": "sha512-mAbxw60d4ffQ4qmRYfkO1xzRBPUEf/72Dgo3qqea0J66nIKuDTLEqQt0ku++SDFlMGMnB6uKDnEG1xD/TDse4Q==", + "requires": { + "@tanstack/query-core": "5.74.4" + } + }, + "@tanstack/react-query-devtools": { + "version": "5.74.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.74.6.tgz", + "integrity": "sha512-vlsDwz4/FsblK0h7VAlXUdJ+9OV+i1n8OLb8CLLAZqu0M9GCnbajytZwsRmns33PXBZ6wQBJ859kg6aajx+e9Q==", + "requires": { + "@tanstack/query-devtools": "5.74.6" + } + }, + "@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, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@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, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@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, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true + }, + "@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, + "@types/prop-types": { + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==" + }, + "@types/react": { + "version": "18.3.20", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz", + "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.3.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.6.tgz", + "integrity": "sha512-nf22//wEbKXusP6E9pfOCDwFdHAX4u172eaJI4YkDRQEZiorm6KfYnSC2SWLDMVWUOWPERmJnN0ujeAfTBLvrw==", + "dev": true + }, + "@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==" + }, + "@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==" + }, + "@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true + }, + "@vitejs/plugin-react": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", + "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", + "dev": true, + "requires": { + "@babel/core": "^7.26.10", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + } + }, + "acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true + }, + "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 + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "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 + }, + "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, + "requires": { + "color-convert": "^2.0.1" + } + }, + "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 + }, + "array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + } + }, + "array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + } + }, + "array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + } + }, + "array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + } + }, + "array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + } + }, + "async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "requires": { + "possible-typed-array-names": "^1.0.0" + } + }, + "axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "requires": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "requires": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + } + }, + "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 + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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, + "optional": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + } + }, + "call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + } + }, + "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==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "caniuse-lite": { + "version": "1.0.30001715", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", + "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "requires": { + "readdirp": "^4.0.1" + } + }, + "clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, + "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, + "requires": { + "color-name": "~1.1.4" + } + }, + "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 + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + } + }, + "data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + } + }, + "data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "requires": { + "ms": "^2.1.3" + } + }, + "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 + }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "optional": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, + "electron-to-chromium": { + "version": "1.5.142", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.142.tgz", + "integrity": "sha512-Ah2HgkTu/9RhTDNThBtzu2Wirdy4DC9b0sMT1pUhbkZQ5U/iwmE+PHZX1MpjD5IkJCc2wSghgGG/B04szAx07w==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + } + }, + "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==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + } + }, + "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==", + "requires": { + "es-errors": "^1.3.0" + } + }, + "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==", + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, + "es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "requires": { + "hasown": "^2.0.2" + } + }, + "es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "requires": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + } + }, + "esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true + }, + "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==" + }, + "eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "dependencies": { + "globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + } + } + }, + "eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "requires": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + } + } + }, + "eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true + }, + "eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "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 + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "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, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "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 + }, + "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 + }, + "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 + }, + "fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "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, + "optional": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "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, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "requires": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + } + }, + "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 + }, + "follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==" + }, + "for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "requires": { + "is-callable": "^1.2.7" + } + }, + "form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "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 + }, + "get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "requires": { + "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" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, + "get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "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, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "requires": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + } + }, + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true + }, + "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 + }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "requires": { + "dunder-proto": "^1.0.0" + } + }, + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "requires": { + "has-symbols": "^1.0.3" + } + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true + }, + "immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==" + }, + "immutable": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", + "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", + "dev": true + }, + "import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + } + }, + "is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "requires": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + } + }, + "is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "requires": { + "has-bigints": "^1.0.2" + } + }, + "is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "requires": { + "hasown": "^2.0.2" + } + }, + "is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + } + }, + "is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "requires": { + "call-bound": "^1.0.3" + } + }, + "is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + } + }, + "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, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true + }, + "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, + "optional": true + }, + "is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + } + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, + "is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true + }, + "is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "requires": { + "call-bound": "^1.0.3" + } + }, + "is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + } + }, + "is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + } + }, + "is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "requires": { + "which-typed-array": "^1.1.16" + } + }, + "is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true + }, + "is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "requires": { + "call-bound": "^1.0.3" + } + }, + "is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==" + }, + "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 + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "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 + }, + "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 + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + } + }, + "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, + "requires": { + "json-buffer": "3.0.1" + } + }, + "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, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "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, + "requires": { + "p-locate": "^5.0.0" + } + }, + "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 + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "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, + "requires": { + "yallist": "^3.0.2" + } + }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "optional": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "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==", + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "optional": true + }, + "node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + } + }, + "object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + } + }, + "object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "requires": { + "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" + } + }, + "own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + } + }, + "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, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "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, + "requires": { + "p-limit": "^3.0.2" + } + }, + "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==", + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "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 + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "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 + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "optional": true + }, + "possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true + }, + "postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "requires": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + } + }, + "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 + }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "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==" + }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "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 + }, + "react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "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==", + "requires": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + } + }, + "react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true + }, + "react-router": { + "version": "6.30.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", + "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", + "requires": { + "@remix-run/router": "1.23.0" + } + }, + "react-router-dom": { + "version": "6.30.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", + "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", + "requires": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.0" + } + }, + "react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, + "readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true + }, + "redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==" + }, + "reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + } + }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + } + }, + "reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" + }, + "resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "requires": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "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==" + }, + "reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "rollup": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", + "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.40.0", + "@rollup/rollup-android-arm64": "4.40.0", + "@rollup/rollup-darwin-arm64": "4.40.0", + "@rollup/rollup-darwin-x64": "4.40.0", + "@rollup/rollup-freebsd-arm64": "4.40.0", + "@rollup/rollup-freebsd-x64": "4.40.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", + "@rollup/rollup-linux-arm-musleabihf": "4.40.0", + "@rollup/rollup-linux-arm64-gnu": "4.40.0", + "@rollup/rollup-linux-arm64-musl": "4.40.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-musl": "4.40.0", + "@rollup/rollup-linux-s390x-gnu": "4.40.0", + "@rollup/rollup-linux-x64-gnu": "4.40.0", + "@rollup/rollup-linux-x64-musl": "4.40.0", + "@rollup/rollup-win32-arm64-msvc": "4.40.0", + "@rollup/rollup-win32-ia32-msvc": "4.40.0", + "@rollup/rollup-win32-x64-msvc": "4.40.0", + "@types/estree": "1.0.7", + "fsevents": "~2.3.2" + } + }, + "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, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + } + }, + "safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + } + }, + "safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + } + }, + "sass": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.87.0.tgz", + "integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==", + "dev": true, + "requires": { + "@parcel/watcher": "^2.4.1", + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, + "scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, + "set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + } + }, + "set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "requires": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + } + }, + "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, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "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 + }, + "side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==" + }, + "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 + }, + "string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + } + }, + "string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + } + }, + "string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "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 + }, + "stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, + "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, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "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, + "optional": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "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, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + } + }, + "typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + } + }, + "typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + } + }, + "typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + } + }, + "unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + } + }, + "update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "requires": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==" + }, + "vite": { + "version": "5.4.18", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz", + "integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==", + "dev": true, + "requires": { + "esbuild": "^0.21.3", + "fsevents": "~2.3.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "requires": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + } + }, + "which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + } + }, + "which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "requires": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + } + }, + "which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + } + }, + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + }, + "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 + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..6d4cc63 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,38 @@ +{ + "name": "rocks-bones-sticks-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0" + }, + "dependencies": { + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "@fontsource/roboto": "^5.0.8", + "@mui/icons-material": "^5.14.19", + "@mui/material": "^5.14.19", + "@reduxjs/toolkit": "^2.0.1", + "@tanstack/react-query": "^5.12.2", + "@tanstack/react-query-devtools": "^5.12.2", + "axios": "^1.6.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-redux": "^9.0.2", + "react-router-dom": "^6.20.1" + }, + "devDependencies": { + "@types/react": "^18.2.37", + "@types/react-dom": "^18.2.15", + "@vitejs/plugin-react": "^4.2.0", + "eslint": "^8.53.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.4", + "sass": "^1.69.5", + "vite": "^5.0.0" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..e2f5a18 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/setup-frontend.sh b/frontend/setup-frontend.sh new file mode 100644 index 0000000..5c24b74 --- /dev/null +++ b/frontend/setup-frontend.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# Create React app using Vite +npm create vite@latest frontend -- --template react + +# Navigate to frontend directory +cd frontend + +# Install core dependencies +npm install \ + react-router-dom \ + @reduxjs/toolkit \ + react-redux \ + @tanstack/react-query \ + @tanstack/react-query-devtools \ + axios \ + @mui/material \ + @mui/icons-material \ + @emotion/react \ + @emotion/styled \ + @fontsource/roboto + +# Install dev dependencies +npm install -D \ + @types/react \ + @types/react-dom \ + @vitejs/plugin-react \ + sass + +# Create frontend project structure +mkdir -p src/{assets,components,features,hooks,layouts,pages,services,store,theme,utils} + +echo "Frontend project setup complete!" \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..a0500a6 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,85 @@ +import { Routes, Route } from 'react-router-dom'; +import { Suspense, lazy } from 'react'; +import { CircularProgress, Box } from '@mui/material'; +import Notifications from './components/Notifications'; +import ProtectedRoute from './components/ProtectedRoute'; + +// Layouts +import MainLayout from './layouts/MainLayout'; +import AuthLayout from './layouts/AuthLayout'; +import AdminLayout from './layouts/AdminLayout'; + +// Pages - lazy loaded to reduce initial bundle size +const HomePage = lazy(() => import('./pages/HomePage')); +const ProductsPage = lazy(() => import('./pages/ProductsPage')); +const ProductDetailPage = lazy(() => import('./pages/ProductDetailPage')); +const CartPage = lazy(() => import('./pages/CartPage')); +const CheckoutPage = lazy(() => import('./pages/CheckoutPage')); +const LoginPage = lazy(() => import('./pages/LoginPage')); +const RegisterPage = lazy(() => import('./pages/RegisterPage')); +const VerifyPage = lazy(() => import('./pages/VerifyPage')); +const AdminDashboardPage = lazy(() => import('./pages/Admin/DashboardPage')); +const AdminProductsPage = lazy(() => import('./pages/Admin/ProductsPage')); +const AdminProductEditPage = lazy(() => import('./pages/Admin/ProductEditPage')); +const AdminCategoriesPage = lazy(() => import('./pages/Admin/CategoriesPage')); +const NotFoundPage = lazy(() => import('./pages/NotFoundPage')); + +// Loading component for suspense fallback +const LoadingComponent = () => ( + + + +); + +function App() { + return ( + }> + + + {/* Main routes with MainLayout */} + }> + } /> + } /> + } /> + + + + } /> + + + + } /> + + + {/* Auth routes with AuthLayout */} + }> + } /> + } /> + + + {/* Verification route - standalone page */} + } /> + + {/* Admin routes with AdminLayout - protected for admins only */} + + + + }> + } /> + } /> + } /> + } /> + } /> + + + {/* Catch-all route for 404s */} + } /> + + + ); +} + +export default App; \ No newline at end of file diff --git a/frontend/src/components/Footer.jsx b/frontend/src/components/Footer.jsx new file mode 100644 index 0000000..c4f2c15 --- /dev/null +++ b/frontend/src/components/Footer.jsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { Box, Container, Grid, Typography, Link, IconButton } from '@mui/material'; +import FacebookIcon from '@mui/icons-material/Facebook'; +import TwitterIcon from '@mui/icons-material/Twitter'; +import InstagramIcon from '@mui/icons-material/Instagram'; +import { Link as RouterLink } from 'react-router-dom'; + +const Footer = () => { + return ( + + theme.palette.mode === 'light' + ? theme.palette.grey[200] + : theme.palette.grey[800], + }} + > + + + + + Rocks, Bones & Sticks + + + Your premier source for natural curiosities + and unique specimens from my backyards. + + + + + + Quick Links + + + Home + + + Shop All + + + Rocks + + + Bones + + + Sticks + + + + + + Connect With Us + + + + + + + + + + + + + + Subscribe to our newsletter for updates on new items and promotions. + + + + + + + © {new Date().getFullYear()} Rocks, Bones & Sticks. All rights reserved. + + + + + ); +}; + +export default Footer; \ No newline at end of file diff --git a/frontend/src/components/ImageUploader.jsx b/frontend/src/components/ImageUploader.jsx new file mode 100644 index 0000000..47d9193 --- /dev/null +++ b/frontend/src/components/ImageUploader.jsx @@ -0,0 +1,225 @@ +import React, { useState } from 'react'; +import { + Box, + Button, + Typography, + CircularProgress, + Alert, + Grid, + IconButton, + Card, + CardMedia, + CardActions, + FormControlLabel, + Checkbox, + Tooltip +} from '@mui/material'; +import DeleteIcon from '@mui/icons-material/Delete'; +import CloudUploadIcon from '@mui/icons-material/CloudUpload'; +import StarIcon from '@mui/icons-material/Star'; +import StarBorderIcon from '@mui/icons-material/StarBorder'; +import { useAuth } from '@hooks/reduxHooks'; +import imageUtils from '@utils/imageUtils'; + +/** + * Image uploader component for product images + * @param {Object} props - Component props + * @param {Array} props.images - Current images + * @param {Function} props.onChange - Callback when images change + * @param {boolean} props.multiple - Whether to allow multiple images + * @param {boolean} props.admin - Whether this is for admin use + * @returns {JSX.Element} - Image uploader component + */ +const ImageUploader = ({ + images = [], + onChange, + multiple = true, + admin = true +}) => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const { apiKey } = useAuth(); + + const handleUpload = async (event) => { + const files = event.target.files; + if (!files || files.length === 0) return; + + setLoading(true); + setError(null); + + try { + if (multiple && files.length > 1) { + // Upload multiple images + const response = await imageUtils.uploadMultipleImages( + Array.from(files), + { apiKey, isProductImage: true } + ); + + // Format new images + const newImages = [...images]; + + response.images.forEach((img, index) => { + newImages.push({ + path: img.imagePath, + isPrimary: images.length === 0 && index === 0, + displayOrder: images.length + index + }); + }); + + onChange(newImages); + } else { + // Upload single image + const response = await imageUtils.uploadImage( + files[0], + { apiKey, isProductImage: true } + ); + + // Add the new image + const newImage = { + path: response.imagePath, + isPrimary: images.length === 0, + displayOrder: images.length + }; + + onChange([...images, newImage]); + } + } catch (err) { + console.error('Image upload failed:', err); + setError(err.message || 'Failed to upload image. Please try again.'); + } finally { + setLoading(false); + } + }; + + const handleRemoveImage = (index) => { + // Get the image to remove + const imageToRemove = images[index]; + + // Extract filename if we need to delete from server + const filename = imageUtils.getFilenameFromPath(imageToRemove.path); + + // Remove from array first + const newImages = [...images]; + newImages.splice(index, 1); + + // If the removed image was primary, make the first one primary + if (imageToRemove.isPrimary && newImages.length > 0) { + newImages[0].isPrimary = true; + } + + // Update state + onChange(newImages); + + // If we have the filename and this is admin mode, delete from server + if (admin && filename) { + imageUtils.deleteImage(filename, { apiKey }) + .catch(err => console.error('Failed to delete image from server:', err)); + } + }; + + const handleSetPrimary = (index) => { + // Update all images, setting only one as primary + const newImages = images.map((img, i) => ({ + ...img, + isPrimary: i === index + })); + + onChange(newImages); + }; + + return ( + + {/* Hidden file input */} + + + {/* Upload button */} + + + {/* Error message */} + {error && ( + + {error} + + )} + + {/* Image grid */} + {images.length > 0 ? ( + + {images.map((image, index) => ( + + + + + + handleSetPrimary(index)} + disabled={image.isPrimary} + > + {image.isPrimary ? : } + + + + + handleRemoveImage(index)} + > + + + + + + + ))} + + ) : ( + + No images uploaded yet. Click the button above to upload. + + )} + + ); +}; + +export default ImageUploader; \ No newline at end of file diff --git a/frontend/src/components/Notifications.jsx b/frontend/src/components/Notifications.jsx new file mode 100644 index 0000000..09cee16 --- /dev/null +++ b/frontend/src/components/Notifications.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Snackbar, Alert } from '@mui/material'; +import { useAppSelector, useAppDispatch } from '../hooks/reduxHooks'; +import { selectNotifications, removeNotification } from '../features/ui/uiSlice'; + +const Notifications = () => { + const notifications = useAppSelector(selectNotifications); + const dispatch = useAppDispatch(); + + const handleClose = (id) => { + dispatch(removeNotification(id)); + }; + + return ( + <> + {notifications.map((notification) => ( + handleClose(notification.id)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} + > + handleClose(notification.id)} + severity={notification.type || 'info'} + sx={{ width: '100%' }} + elevation={6} + variant="filled" + > + {notification.message} + + + ))} + + ); +}; + +export default Notifications; \ No newline at end of file diff --git a/frontend/src/components/ProductImage.jsx b/frontend/src/components/ProductImage.jsx new file mode 100644 index 0000000..8a7a411 --- /dev/null +++ b/frontend/src/components/ProductImage.jsx @@ -0,0 +1,67 @@ +import React, { useState } from 'react'; +import { Box } from '@mui/material'; +import imageUtils from '@utils/imageUtils'; + +/** + * Component to display product images with fallback handling + * @param {Object} props - Component props + * @param {Array|Object|string} props.images - Product images array, single image object, or image path + * @param {string} props.alt - Alt text for the image + * @param {Object} props.sx - Additional styling props + * @param {string} props.placeholderImage - Placeholder image to use when no image is available + * @returns {JSX.Element} - ProductImage component + */ +const ProductImage = ({ + images, + alt = 'Product image', + sx = {}, + placeholderImage = '/placeholder.jpg', + ...rest +}) => { + const [imageError, setImageError] = useState(false); + + // Determine which image to use + let imageSrc = placeholderImage; + + // Handle different types of image inputs + if (Array.isArray(images) && images.length > 0) { + // Find primary image in the array + const primaryImage = images.find(img => img.isPrimary || img.is_primary) || images[0]; + const imagePath = primaryImage.path || primaryImage.image_path || primaryImage; + imageSrc = imageUtils.getImageUrl(imagePath, placeholderImage); + } else if (typeof images === 'object' && images !== null) { + // Single image object + const imagePath = images.path || images.image_path; + imageSrc = imageUtils.getImageUrl(imagePath, placeholderImage); + } else if (typeof images === 'string' && images) { + // Direct image path + imageSrc = imageUtils.getImageUrl(images, placeholderImage); + } + + // Handle image load errors + const handleError = () => { + console.error(`Failed to load image: ${imageSrc}`); + if (!imageError) { + setImageError(true); + } + }; + + return ( + + ); +}; + +export default ProductImage; \ No newline at end of file diff --git a/frontend/src/components/ProtectedRoute.jsx b/frontend/src/components/ProtectedRoute.jsx new file mode 100644 index 0000000..8de9a32 --- /dev/null +++ b/frontend/src/components/ProtectedRoute.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; +import { useAuth } from '../hooks/reduxHooks'; +import { CircularProgress, Box } from '@mui/material'; + +/** + * ProtectedRoute component to handle authenticated routes + * + * @param {Object} props - Component props + * @param {ReactNode} props.children - Child components to render when authenticated + * @param {boolean} [props.requireAdmin=false] - Whether the route requires admin privileges + * @param {string} [props.redirectTo='/auth/login'] - Where to redirect if not authenticated + * @returns {ReactNode} The protected route + */ +const ProtectedRoute = ({ + children, + requireAdmin = false, + redirectTo = '/auth/login' +}) => { + const { isAuthenticated, isAdmin, loading } = useAuth(); + const location = useLocation(); + + // Show loading state + if (loading) { + return ( + + + + ); + } + + // Check authentication + if (!isAuthenticated) { + // Redirect to login, but save the current location so we can redirect back after login + return ; + } + + // Check admin privileges if required + if (requireAdmin && !isAdmin) { + // Redirect to home if not admin + return ; + } + + // Render children if authenticated and has required privileges + return children; +}; + +export default ProtectedRoute; \ No newline at end of file diff --git a/frontend/src/config.js b/frontend/src/config.js new file mode 100644 index 0000000..0105a24 --- /dev/null +++ b/frontend/src/config.js @@ -0,0 +1,21 @@ +const config = { + // App information + appName: import.meta.env.VITE_APP_NAME || 'Rocks, Bones & Sticks', + + // API connection + apiUrl: import.meta.env.VITE_API_URL || '/api', + + // Environment + environment: import.meta.env.VITE_ENVIRONMENT || 'beta', + isDevelopment: import.meta.env.DEV, + isProduction: import.meta.env.PROD, + + // Site configuration (domain and protocol based on environment) + site: { + domain: import.meta.env.VITE_ENVIRONMENT === 'prod' ? 'rocks.2many.ca' : 'localhost:3000', + protocol: import.meta.env.VITE_ENVIRONMENT === 'prod' ? 'https' : 'http', + apiDomain: import.meta.env.VITE_ENVIRONMENT === 'prod' ? 'api.rocks.2many.ca' : 'localhost:4000' + } + }; + + export default config; \ No newline at end of file diff --git a/frontend/src/features/auth/authSlice.js b/frontend/src/features/auth/authSlice.js new file mode 100644 index 0000000..24aa1de --- /dev/null +++ b/frontend/src/features/auth/authSlice.js @@ -0,0 +1,59 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + user: JSON.parse(localStorage.getItem('user')) || null, + apiKey: localStorage.getItem('apiKey') || null, + isAdmin: localStorage.getItem('isAdmin') === 'true', + isAuthenticated: !!localStorage.getItem('apiKey'), + loading: false, + error: null, +}; + +export const authSlice = createSlice({ + name: 'auth', + initialState, + reducers: { + loginStart: (state) => { + state.loading = true; + state.error = null; + }, + loginSuccess: (state, action) => { + state.loading = false; + state.isAuthenticated = true; + state.user = action.payload.user; + state.apiKey = action.payload.apiKey; + state.isAdmin = action.payload.isAdmin; + localStorage.setItem('apiKey', action.payload.apiKey); + localStorage.setItem('isAdmin', action.payload.isAdmin); + localStorage.setItem('user', JSON.stringify(action.payload.user)); + }, + loginFailed: (state, action) => { + state.loading = false; + state.error = action.payload; + }, + logout: (state) => { + state.isAuthenticated = false; + state.user = null; + state.apiKey = null; + state.isAdmin = false; + localStorage.removeItem('apiKey'); + localStorage.removeItem('isAdmin'); + localStorage.removeItem('user'); + }, + clearError: (state) => { + state.error = null; + }, + }, +}); + +export const { loginStart, loginSuccess, loginFailed, logout, clearError } = authSlice.actions; + +// Selectors +export const selectIsAuthenticated = (state) => state.auth.isAuthenticated; +export const selectIsAdmin = (state) => state.auth.isAdmin; +export const selectCurrentUser = (state) => state.auth.user; +export const selectApiKey = (state) => state.auth.apiKey; +export const selectAuthLoading = (state) => state.auth.loading; +export const selectAuthError = (state) => state.auth.error; + +export default authSlice.reducer; \ No newline at end of file diff --git a/frontend/src/features/cart/cartSlice.js b/frontend/src/features/cart/cartSlice.js new file mode 100644 index 0000000..b7a1562 --- /dev/null +++ b/frontend/src/features/cart/cartSlice.js @@ -0,0 +1,58 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + id: null, + items: [], + itemCount: 0, + total: 0, + loading: false, + error: null, +}; + +export const cartSlice = createSlice({ + name: 'cart', + initialState, + reducers: { + cartLoading: (state) => { + state.loading = true; + state.error = null; + }, + cartLoadingFailed: (state, action) => { + state.loading = false; + state.error = action.payload; + }, + updateCart: (state, action) => { + state.loading = false; + state.id = action.payload.id; + state.items = action.payload.items; + state.itemCount = action.payload.itemCount; + state.total = action.payload.total; + }, + clearCart: (state) => { + state.id = null; + state.items = []; + state.itemCount = 0; + state.total = 0; + }, + clearCartError: (state) => { + state.error = null; + }, + }, +}); + +export const { + cartLoading, + cartLoadingFailed, + updateCart, + clearCart, + clearCartError +} = cartSlice.actions; + +// Selectors +export const selectCartItems = (state) => state.cart.items; +export const selectCartItemCount = (state) => state.cart.itemCount; +export const selectCartTotal = (state) => state.cart.total; +export const selectCartLoading = (state) => state.cart.loading; +export const selectCartError = (state) => state.cart.error; + +export default cartSlice.reducer; \ No newline at end of file diff --git a/frontend/src/features/ui/uiSlice.js b/frontend/src/features/ui/uiSlice.js new file mode 100644 index 0000000..4e49a7b --- /dev/null +++ b/frontend/src/features/ui/uiSlice.js @@ -0,0 +1,59 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + notifications: [], + darkMode: localStorage.getItem('darkMode') === 'true', + mobileMenuOpen: false, +}; + +export const uiSlice = createSlice({ + name: 'ui', + initialState, + reducers: { + addNotification: (state, action) => { + state.notifications.push({ + id: Date.now(), + ...action.payload, + }); + }, + removeNotification: (state, action) => { + state.notifications = state.notifications.filter( + (notification) => notification.id !== action.payload + ); + }, + clearNotifications: (state) => { + state.notifications = []; + }, + toggleDarkMode: (state) => { + state.darkMode = !state.darkMode; + localStorage.setItem('darkMode', state.darkMode); + }, + setDarkMode: (state, action) => { + state.darkMode = action.payload; + localStorage.setItem('darkMode', action.payload); + }, + toggleMobileMenu: (state) => { + state.mobileMenuOpen = !state.mobileMenuOpen; + }, + closeMobileMenu: (state) => { + state.mobileMenuOpen = false; + }, + }, +}); + +export const { + addNotification, + removeNotification, + clearNotifications, + toggleDarkMode, + setDarkMode, + toggleMobileMenu, + closeMobileMenu, +} = uiSlice.actions; + +// Selectors +export const selectNotifications = (state) => state.ui.notifications; +export const selectDarkMode = (state) => state.ui.darkMode; +export const selectMobileMenuOpen = (state) => state.ui.mobileMenuOpen; + +export default uiSlice.reducer; \ No newline at end of file diff --git a/frontend/src/hooks/apiHooks.js b/frontend/src/hooks/apiHooks.js new file mode 100644 index 0000000..25aa4af --- /dev/null +++ b/frontend/src/hooks/apiHooks.js @@ -0,0 +1,271 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import productService, { productAdminService } from '../services/productService'; +import authService from '../services/authService'; +import cartService from '../services/cartService'; +import { useAuth, useCart, useNotification } from './reduxHooks'; + +// Product hooks +export const useProducts = (params) => { + return useQuery({ + queryKey: ['products', params], + queryFn: () => productService.getAllProducts(params), + }); +}; + +export const useProduct = (id) => { + return useQuery({ + queryKey: ['product', id], + queryFn: () => productService.getProductById(id), + enabled: !!id, + }); +}; + +export const useCategories = () => { + return useQuery({ + queryKey: ['categories'], + queryFn: () => productService.getAllCategories(), + }); +}; + +export const useTags = () => { + return useQuery({ + queryKey: ['tags'], + queryFn: () => productService.getAllTags(), + }); +}; + +export const useProductsByCategory = (categoryName) => { + return useQuery({ + queryKey: ['products', 'category', categoryName], + queryFn: () => productService.getProductsByCategory(categoryName), + enabled: !!categoryName, + }); +}; + +// Admin product hooks +export const useCreateProduct = () => { + const queryClient = useQueryClient(); + const notification = useNotification(); + + return useMutation({ + mutationFn: (productData) => productAdminService.createProduct(productData), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['products'] }); + notification.showNotification('Product created successfully', 'success'); + }, + onError: (error) => { + notification.showNotification( + error.message || 'Failed to create product', + 'error' + ); + }, + }); +}; + +export const useUpdateProduct = () => { + const queryClient = useQueryClient(); + const notification = useNotification(); + + return useMutation({ + mutationFn: ({ id, productData }) => productAdminService.updateProduct(id, productData), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ['products'] }); + queryClient.invalidateQueries({ queryKey: ['product', variables.id] }); + notification.showNotification('Product updated successfully', 'success'); + }, + onError: (error) => { + notification.showNotification( + error.message || 'Failed to update product', + 'error' + ); + }, + }); +}; + +export const useDeleteProduct = () => { + const queryClient = useQueryClient(); + const notification = useNotification(); + + return useMutation({ + mutationFn: (id) => productAdminService.deleteProduct(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['products'] }); + notification.showNotification('Product deleted successfully', 'success'); + }, + onError: (error) => { + notification.showNotification( + error.message || 'Failed to delete product', + 'error' + ); + }, + }); +}; + +// Auth hooks +export const useRegister = () => { + const notification = useNotification(); + + return useMutation({ + mutationFn: (userData) => authService.register(userData), + onSuccess: () => { + notification.showNotification('Registration successful', 'success'); + }, + onError: (error) => { + notification.showNotification( + error.message || 'Registration failed', + 'error' + ); + }, + }); +}; + +export const useRequestLoginCode = () => { + const notification = useNotification(); + + return useMutation({ + mutationFn: (email) => authService.requestLoginCode(email), + onSuccess: () => { + notification.showNotification('Login code sent to your email', 'success'); + }, + onError: (error) => { + notification.showNotification( + error.message || 'Failed to send login code', + 'error' + ); + }, + }); +}; + +export const useVerifyCode = () => { + const { login } = useAuth(); + const notification = useNotification(); + + return useMutation({ + mutationFn: (verifyData) => authService.verifyCode(verifyData), + onSuccess: (data) => { + login(data.userId, data.apiKey, data.isAdmin); + notification.showNotification('Login successful', 'success'); + }, + onError: (error) => { + notification.showNotification( + error.message || 'Invalid verification code', + 'error' + ); + }, + }); +}; + +export const useLogout = () => { + const { logout, user } = useAuth(); + const notification = useNotification(); + + return useMutation({ + mutationFn: () => authService.logout(user?.id), + onSuccess: () => { + logout(); + notification.showNotification('Logged out successfully', 'success'); + }, + onError: () => { + // Force logout even if API call fails + logout(); + }, + }); +}; + +// Cart hooks +export const useGetCart = (userId) => { + const { updateCart } = useCart(); + + return useQuery({ + queryKey: ['cart', userId], + queryFn: () => cartService.getCart(userId), + enabled: !!userId, + onSuccess: (data) => { + updateCart(data); + }, + }); +}; + +export const useAddToCart = () => { + const queryClient = useQueryClient(); + const { updateCart } = useCart(); + const notification = useNotification(); + + return useMutation({ + mutationFn: (cartItemData) => cartService.addToCart(cartItemData), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ['cart'] }); + updateCart(data); + notification.showNotification('Item added to cart', 'success'); + }, + onError: (error) => { + notification.showNotification( + error.message || 'Failed to add item to cart', + 'error' + ); + }, + }); +}; + +export const useUpdateCartItem = () => { + const queryClient = useQueryClient(); + const { updateCart } = useCart(); + const notification = useNotification(); + + return useMutation({ + mutationFn: (updateData) => cartService.updateCartItem(updateData), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ['cart'] }); + updateCart(data); + notification.showNotification('Cart updated', 'success'); + }, + onError: (error) => { + notification.showNotification( + error.message || 'Failed to update cart', + 'error' + ); + }, + }); +}; + +export const useClearCart = () => { + const queryClient = useQueryClient(); + const { clearCart } = useCart(); + const notification = useNotification(); + + return useMutation({ + mutationFn: (userId) => cartService.clearCart(userId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['cart'] }); + clearCart(); + notification.showNotification('Cart cleared', 'success'); + }, + onError: (error) => { + notification.showNotification( + error.message || 'Failed to clear cart', + 'error' + ); + }, + }); +}; + +export const useCheckout = () => { + const queryClient = useQueryClient(); + const { clearCart } = useCart(); + const notification = useNotification(); + + return useMutation({ + mutationFn: (checkoutData) => cartService.checkout(checkoutData), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['cart'] }); + clearCart(); + notification.showNotification('Order placed successfully', 'success'); + }, + onError: (error) => { + notification.showNotification( + error.message || 'Checkout failed', + 'error' + ); + }, + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/categoryAdminHooks.js b/frontend/src/hooks/categoryAdminHooks.js new file mode 100644 index 0000000..667dcde --- /dev/null +++ b/frontend/src/hooks/categoryAdminHooks.js @@ -0,0 +1,98 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { categoryAdminService } from '../services/categoryAdminService'; +import { useNotification } from './reduxHooks'; + +/** + * Hook for fetching all categories + */ +export const useAdminCategories = () => { + return useQuery({ + queryKey: ['admin-categories'], + queryFn: () => categoryAdminService.getAllCategories(), + }); +}; + +/** + * Hook for fetching a single category by ID + */ +export const useAdminCategory = (id) => { + return useQuery({ + queryKey: ['admin-category', id], + queryFn: () => categoryAdminService.getCategoryById(id), + enabled: !!id, + }); +}; + +/** + * Hook for creating a new category + */ +export const useCreateCategory = () => { + const queryClient = useQueryClient(); + const notification = useNotification(); + + return useMutation({ + mutationFn: (categoryData) => categoryAdminService.createCategory(categoryData), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ['admin-categories'] }); + queryClient.invalidateQueries({ queryKey: ['categories'] }); + notification.showNotification('Category created successfully!', 'success'); + return data; + }, + onError: (error) => { + notification.showNotification( + error.message || 'Failed to create category', + 'error' + ); + throw error; + }, + }); +}; + +/** + * Hook for updating a category + */ +export const useUpdateCategory = () => { + const queryClient = useQueryClient(); + const notification = useNotification(); + + return useMutation({ + mutationFn: ({ id, categoryData }) => categoryAdminService.updateCategory(id, categoryData), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ['admin-categories'] }); + queryClient.invalidateQueries({ queryKey: ['categories'] }); + notification.showNotification('Category updated successfully!', 'success'); + return data; + }, + onError: (error) => { + notification.showNotification( + error.message || 'Failed to update category', + 'error' + ); + throw error; + }, + }); +}; + +/** + * Hook for deleting a category + */ +export const useDeleteCategory = () => { + const queryClient = useQueryClient(); + const notification = useNotification(); + + return useMutation({ + mutationFn: (id) => categoryAdminService.deleteCategory(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin-categories'] }); + queryClient.invalidateQueries({ queryKey: ['categories'] }); + notification.showNotification('Category deleted successfully!', 'success'); + }, + onError: (error) => { + notification.showNotification( + error.message || 'Failed to delete category', + 'error' + ); + throw error; + }, + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/reduxHooks.js b/frontend/src/hooks/reduxHooks.js new file mode 100644 index 0000000..2c3b54a --- /dev/null +++ b/frontend/src/hooks/reduxHooks.js @@ -0,0 +1,105 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { useMemo } from 'react'; + +// Use throughout your app instead of plain `useDispatch` and `useSelector` +export const useAppDispatch = () => useDispatch(); + +export const useAppSelector = useSelector; + +// Create a custom hook for notifications +export const useNotification = () => { + const dispatch = useAppDispatch(); + + return useMemo(() => ({ + showNotification: (message, type = 'info', duration = 3000) => { + const id = Date.now(); + dispatch({ + type: 'ui/addNotification', + payload: { message, type, duration, id }, + }); + + if (duration !== null) { + setTimeout(() => { + dispatch({ + type: 'ui/removeNotification', + payload: id, + }); + }, duration); + } + + return id; + }, + + closeNotification: (id) => { + dispatch({ + type: 'ui/removeNotification', + payload: id, + }); + }, + + clearNotifications: () => { + dispatch({ type: 'ui/clearNotifications' }); + }, + }), [dispatch]); +}; + +// Custom hook for dark mode +export const useDarkMode = () => { + const darkMode = useAppSelector((state) => state.ui.darkMode); + const dispatch = useAppDispatch(); + + return [ + darkMode, + () => dispatch({ type: 'ui/toggleDarkMode' }), + (value) => dispatch({ type: 'ui/setDarkMode', payload: value }), + ]; +}; + +// Custom hook for auth state +export const useAuth = () => { + const isAuthenticated = useAppSelector((state) => state.auth.isAuthenticated); + const isAdmin = useAppSelector((state) => state.auth.isAdmin); + const user = useAppSelector((state) => state.auth.user); + const apiKey = useAppSelector((state) => state.auth.apiKey); + const loading = useAppSelector((state) => state.auth.loading); + const error = useAppSelector((state) => state.auth.error); + const dispatch = useAppDispatch(); + + return { + isAuthenticated, + isAdmin, + user, + apiKey, + loading, + error, + login: (user, apiKey, isAdmin) => + dispatch({ + type: 'auth/loginSuccess', + payload: { user, apiKey, isAdmin } + }), + logout: () => dispatch({ type: 'auth/logout' }), + clearError: () => dispatch({ type: 'auth/clearError' }), + }; +}; + +// Custom hook for cart state +export const useCart = () => { + const items = useAppSelector((state) => state.cart.items); + const itemCount = useAppSelector((state) => state.cart.itemCount); + const total = useAppSelector((state) => state.cart.total); + const loading = useAppSelector((state) => state.cart.loading); + const error = useAppSelector((state) => state.cart.error); + const dispatch = useAppDispatch(); + + return { + items, + itemCount, + total, + loading, + error, + updateCart: (cartData) => + dispatch({ type: 'cart/updateCart', payload: cartData }), + clearCart: () => dispatch({ type: 'cart/clearCart' }), + clearError: () => dispatch({ type: 'cart/clearCartError' }), + }; +}; \ No newline at end of file diff --git a/frontend/src/layouts/AdminLayout.jsx b/frontend/src/layouts/AdminLayout.jsx new file mode 100644 index 0000000..10d0078 --- /dev/null +++ b/frontend/src/layouts/AdminLayout.jsx @@ -0,0 +1,214 @@ +import React, { useState, useEffect } from 'react'; +import { Outlet, useNavigate } from 'react-router-dom'; +import { Box, Drawer, AppBar, Toolbar, List, Typography, Divider, + IconButton, ListItem, ListItemIcon, ListItemText, Container, + useMediaQuery, CssBaseline } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import MenuIcon from '@mui/icons-material/Menu'; +import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; +import DashboardIcon from '@mui/icons-material/Dashboard'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; +import PeopleIcon from '@mui/icons-material/People'; +import BarChartIcon from '@mui/icons-material/BarChart'; +import CategoryIcon from '@mui/icons-material/Category'; +import HomeIcon from '@mui/icons-material/Home'; +import AddCircleIcon from '@mui/icons-material/AddCircle'; +import LogoutIcon from '@mui/icons-material/Logout'; +import Brightness4Icon from '@mui/icons-material/Brightness4'; +import Brightness7Icon from '@mui/icons-material/Brightness7'; +import ClassIcon from '@mui/icons-material/Class'; +import { Link as RouterLink } from 'react-router-dom'; +import { useAuth, useDarkMode } from '../hooks/reduxHooks'; + +const drawerWidth = 240; + +const AdminLayout = () => { + const [open, setOpen] = useState(true); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const navigate = useNavigate(); + const { isAuthenticated, isAdmin, logout } = useAuth(); + const [darkMode, toggleDarkMode] = useDarkMode(); + + // Force drawer closed on mobile + useEffect(() => { + if (isMobile) { + setOpen(false); + } + }, [isMobile]); + + // Redirect if not admin + useEffect(() => { + if (!isAuthenticated || !isAdmin) { + navigate('/auth/login'); + } + }, [isAuthenticated, isAdmin, navigate]); + + const handleDrawerOpen = () => { + setOpen(true); + }; + + const handleDrawerClose = () => { + setOpen(false); + }; + + const handleLogout = () => { + logout(); + navigate('/'); + }; + + const mainListItems = [ + { text: 'Dashboard', icon: , path: '/admin' }, + { text: 'Products', icon: , path: '/admin/products' }, + { text: 'Add Product', icon: , path: '/admin/products/new' }, + { text: 'Categories', icon: , path: '/admin/categories' }, + { text: 'Orders', icon: , path: '/admin/orders' }, + { text: 'Customers', icon: , path: '/admin/customers' }, + { text: 'Reports', icon: , path: '/admin/reports' }, + ]; + + const secondaryListItems = [ + { text: 'Visit Site', icon: , path: '/' }, + { text: darkMode ? 'Light Mode' : 'Dark Mode', icon: darkMode ? : , onClick: toggleDarkMode }, + { text: 'Logout', icon: , onClick: handleLogout }, + ]; + + return ( + + + theme.zIndex.drawer + 1, + transition: (theme) => theme.transitions.create(['width', 'margin'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + ...(open && { + marginLeft: drawerWidth, + width: `calc(100% - ${drawerWidth}px)`, + transition: (theme) => theme.transitions.create(['width', 'margin'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + }), + }} + > + + + + + + Admin Dashboard + + + + theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + boxSizing: 'border-box', + ...(!open && { + overflowX: 'hidden', + transition: (theme) => theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + width: theme.spacing(7), + [theme.breakpoints.up('sm')]: { + width: theme.spacing(9), + }, + }), + }, + }} + > + + + + + + + + {mainListItems.map((item) => ( + + {item.icon} + + + ))} + + + + {secondaryListItems.map((item) => ( + + {item.icon} + + + ))} + + + + theme.palette.mode === 'light' + ? theme.palette.grey[100] + : theme.palette.grey[900], + flexGrow: 1, + height: '100vh', + overflow: 'auto', + }} + > + + + + + + + ); +}; + +export default AdminLayout; \ No newline at end of file diff --git a/frontend/src/layouts/AuthLayout.jsx b/frontend/src/layouts/AuthLayout.jsx new file mode 100644 index 0000000..3cc0766 --- /dev/null +++ b/frontend/src/layouts/AuthLayout.jsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { Outlet } from 'react-router-dom'; +import { Box, Container, Paper, Typography, Button } from '@mui/material'; +import { Link as RouterLink } from 'react-router-dom'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; + +const AuthLayout = () => { + return ( + theme.palette.mode === 'dark' ? 'background.default' : 'grey.100', + }} + > + + + + + + Rocks, Bones & Sticks + + + + + + + + + + theme.palette.mode === 'light' + ? theme.palette.grey[200] + : theme.palette.grey[800], + }} + > + + + © {new Date().getFullYear()} Rocks, Bones & Sticks. All rights reserved. + + + + + ); +}; + +export default AuthLayout; \ No newline at end of file diff --git a/frontend/src/layouts/MainLayout.jsx b/frontend/src/layouts/MainLayout.jsx new file mode 100644 index 0000000..0810864 --- /dev/null +++ b/frontend/src/layouts/MainLayout.jsx @@ -0,0 +1,219 @@ +import React, { useState } from 'react'; +import { Outlet } from 'react-router-dom'; +import { Box, Container, AppBar, Toolbar, Typography, Button, + IconButton, Drawer, List, ListItem, ListItemIcon, ListItemText, + Divider, Badge, useMediaQuery } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import MenuIcon from '@mui/icons-material/Menu'; +import HomeIcon from '@mui/icons-material/Home'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; +import CategoryIcon from '@mui/icons-material/Category'; +import PersonIcon from '@mui/icons-material/Person'; +import LoginIcon from '@mui/icons-material/Login'; +import LogoutIcon from '@mui/icons-material/Logout'; +import DashboardIcon from '@mui/icons-material/Dashboard'; +import Brightness4Icon from '@mui/icons-material/Brightness4'; +import Brightness7Icon from '@mui/icons-material/Brightness7'; +import { Link as RouterLink, useNavigate } from 'react-router-dom'; +import { useAuth, useCart, useDarkMode } from '../hooks/reduxHooks'; +import Footer from '../components/Footer'; + +const MainLayout = () => { + const [drawerOpen, setDrawerOpen] = useState(false); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const navigate = useNavigate(); + + const { isAuthenticated, isAdmin, logout } = useAuth(); + const { itemCount } = useCart(); + const [darkMode, toggleDarkMode] = useDarkMode(); + + const handleDrawerToggle = () => { + setDrawerOpen(!drawerOpen); + }; + + const handleLogout = () => { + logout(); + navigate('/'); + }; + + const mainMenu = [ + { text: 'Home', icon: , path: '/' }, + { text: 'Products', icon: , path: '/products' }, + { text: 'Cart', icon: , path: '/cart', badge: itemCount > 0 ? itemCount : null }, + ]; + + const authMenu = isAuthenticated ? + [ + { text: 'Logout', icon: , onClick: handleLogout } + ] : [ + { text: 'Login', icon: , path: '/auth/login' }, + { text: 'Register', icon: , path: '/auth/register' } + ]; + + // Add admin menu if user is admin + if (isAuthenticated && isAdmin) { + mainMenu.push( + { text: 'Admin Dashboard', icon: , path: '/admin' } + ); + } + + const drawer = ( + + + + Rocks, Bones & Sticks + + + + + {mainMenu.map((item) => ( + + + {item.badge ? ( + + {item.icon} + + ) : ( + item.icon + )} + + + + ))} + + + + + + {darkMode ? : } + + + + {authMenu.map((item) => ( + + {item.icon} + + + ))} + + + ); + + return ( + + + + + + + + + Rocks, Bones & Sticks + + + {/* Desktop navigation */} + {!isMobile && ( + + {mainMenu.map((item) => ( + + ))} + + + {darkMode ? : } + + + {authMenu.map((item) => ( + item.path ? ( + + ) : ( + + ) + ))} + + )} + + + + + {drawer} + + + + + + + + +