├── .env ├── .gitignore ├── README.md ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── public └── vite.svg ├── server ├── db.json ├── package-lock.json ├── package.json └── server.js ├── src ├── App.tsx ├── api │ ├── axiosClient.ts │ ├── endpoints.ts │ └── queryClient.ts ├── assets │ └── react.svg ├── components │ └── layout │ │ ├── Navbar.tsx │ │ └── Sidebar.tsx ├── index.css ├── main.tsx ├── pages │ ├── index.ts │ ├── orders │ │ ├── components │ │ │ ├── OrderCard.tsx │ │ │ ├── OrderDetail.tsx │ │ │ ├── OrderDetailView.tsx │ │ │ ├── OrderForm.tsx │ │ │ └── OrderList.tsx │ │ └── queries │ │ │ └── OrderQueries.ts │ ├── products │ │ ├── components │ │ │ ├── Pagination.tsx │ │ │ ├── ProductCard.tsx │ │ │ ├── ProductDetail.tsx │ │ │ ├── ProductDetailView.tsx │ │ │ ├── ProductForm.tsx │ │ │ └── ProductList.tsx │ │ ├── queries │ │ │ └── ProductQueries.ts │ │ ├── services │ │ │ └── ProductService.ts │ │ └── types.ts │ ├── shared │ │ └── components │ │ │ └── Pagination.tsx │ └── users │ │ ├── components │ │ ├── UserCard.tsx │ │ ├── UserDetail.tsx │ │ ├── UserDetailView.tsx │ │ ├── UserForm.tsx │ │ └── UserList.tsx │ │ └── queries │ │ └── UserQueries.ts ├── types │ └── index.ts └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.env: -------------------------------------------------------------------------------- 1 | # API Configuration 2 | VITE_BASE_URL=http://localhost:3001 3 | 4 | # Authentication 5 | VITE_AUTH_ENABLED=true 6 | VITE_TOKEN_REFRESH_INTERVAL=300000 7 | 8 | # Feature Flags 9 | VITE_ENABLE_QUERY_DEVTOOLS=true 10 | VITE_ENABLE_ERROR_MONITORING=true 11 | 12 | # Cache Configuration 13 | VITE_QUERY_STALE_TIME=30000 14 | VITE_QUERY_CACHE_TIME=300000 15 | 16 | # API Rate Limiting 17 | VITE_API_RATE_LIMIT=100 18 | VITE_API_TIMEOUT=10000 19 | 20 | # Optional Services (uncomment and add your values as needed) 21 | # VITE_SENTRY_DSN=your_sentry_dsn 22 | # VITE_ANALYTICS_ID=your_analytics_id -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Axios React Query Example 2 | 3 | A comprehensive example demonstrating best practices for data fetching using Axios and React Query. 4 | 5 | ## Project Structure 6 | 7 | ``` 8 | src/ 9 | ├── api/ 10 | │ ├── axiosClient.ts # Axios instance and interceptors 11 | │ ├── endpoints.ts # API endpoint definitions 12 | │ └── queryClient.ts # React Query client configuration 13 | ├── pages/ 14 | │ ├── products/ 15 | │ │ ├── components/ 16 | │ │ │ ├── ProductList.tsx 17 | │ │ │ ├── ProductDetail.tsx 18 | │ │ │ ├── ProductForm.tsx 19 | │ │ │ └── ProductCard.tsx 20 | │ │ ├── queries/ 21 | │ │ │ └── ProductQueries.ts 22 | │ │ └── services/ 23 | │ │ └── ProductService.ts # Page-specific service 24 | │ ├── orders/ 25 | │ │ ├── components/ 26 | │ │ │ ├── OrderList.tsx 27 | │ │ │ ├── OrderDetail.tsx 28 | │ │ │ ├── OrderForm.tsx 29 | │ │ │ └── OrderCard.tsx 30 | │ │ ├── queries/ 31 | │ │ │ └── OrderQueries.ts 32 | │ │ └── services/ 33 | │ │ └── OrderService.ts 34 | │ └── users/ 35 | │ ├── components/ 36 | │ │ ├── UserList.tsx 37 | │ │ ├── UserDetail.tsx 38 | │ │ ├── UserForm.tsx 39 | │ │ └── UserCard.tsx 40 | │ ├── queries/ 41 | │ │ └── UserQueries.ts 42 | │ └── services/ 43 | │ └── UserService.ts 44 | ├── types/ 45 | │ └── index.ts 46 | └── components/ 47 | ├── layout/ 48 | │ ├── Sidebar.tsx 49 | │ └── Header.tsx 50 | └── shared/ 51 | ├── Pagination.tsx 52 | └── ErrorBoundary.tsx 53 | 54 | ``` 55 | 56 | ## Features 57 | 58 | - Centralized API configuration with Axios 59 | - Efficient data fetching and caching with React Query 60 | - TypeScript support 61 | - Error handling and retry logic 62 | - Authentication token management 63 | - Request/Response interceptors 64 | - Global error notifications 65 | - Pagination support 66 | - Form handling 67 | - Optimistic updates 68 | - Real-time data synchronization 69 | 70 | ## Tech Stack 71 | 72 | - React 19 73 | - TypeScript 74 | - Vite 75 | - TanStack Query (React Query) 76 | - Axios 77 | - Tailwind CSS 78 | - JSON Server (Mock API) 79 | 80 | ## Getting Started 81 | 82 | 1. Clone the repository: 83 | 84 | ```bash 85 | git clone https://github.com/yourusername/axios-react-query-example.git 86 | cd axios-react-query-example 87 | ``` 88 | 89 | 2. Install dependencies: 90 | 91 | ```bash 92 | npm install 93 | ``` 94 | 95 | 3. Start the mock API server: 96 | 97 | ```bash 98 | cd server 99 | npm install 100 | npm start 101 | ``` 102 | 103 | 4. In a new terminal, start the development server: 104 | 105 | ```bash 106 | npm run dev 107 | ``` 108 | 109 | ## Development 110 | 111 | ### Environment Setup 112 | 113 | Create a `.env` file in the root directory: 114 | 115 | ```env 116 | VITE_APP_BASE_URL=http://localhost:3000 117 | ``` 118 | 119 | ### Available Scripts 120 | 121 | - `npm run dev` - Start development server 122 | - `npm run build` - Build for production 123 | - `npm run preview` - Preview production build 124 | - `npm run lint` - Lint code 125 | - `npm run format` - Format code 126 | 127 | ## Best Practices 128 | 129 | ### Code Organization 130 | 131 | - Feature-based folder structure 132 | - Separation of concerns 133 | - Reusable components and hooks 134 | - Type safety with TypeScript 135 | 136 | ### Data Fetching 137 | 138 | - Centralized API configuration 139 | - Query caching and invalidation 140 | - Optimistic updates 141 | - Error handling and retries 142 | 143 | ### UI/UX 144 | 145 | - Responsive design 146 | - Loading states 147 | - Error boundaries 148 | - Form validation 149 | 150 | ## API Documentation 151 | 152 | The mock API server provides the following endpoints: 153 | 154 | - `/api/products` - Product management 155 | - `/api/orders` - Order management 156 | - `/api/users` - User management 157 | 158 | Each endpoint supports standard CRUD operations. 159 | 160 | ## License 161 | 162 | MIT 163 | 164 | ``` 165 | 166 | ``` 167 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import globals from "globals"; 3 | import reactHooks from "eslint-plugin-react-hooks"; 4 | import reactRefresh from "eslint-plugin-react-refresh"; 5 | import tseslint from "typescript-eslint"; 6 | 7 | export default tseslint.config( 8 | { ignores: ["dist"] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ["**/*.{ts,tsx}"], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | "react-hooks": reactHooks, 18 | "react-refresh": reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | "react-refresh/only-export-components": [ 23 | "warn", 24 | { allowConstantExport: true }, 25 | ], 26 | "@typescript-eslint/no-explicit-any": "warn", 27 | }, 28 | } 29 | ); 30 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "axios-react-query-example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview", 11 | "server": "node server/server.js" 12 | }, 13 | "dependencies": { 14 | "@tailwindcss/vite": "^4.0.17", 15 | "@tanstack/react-query": "^5.69.0", 16 | "axios": "^1.8.4", 17 | "react": "^19.0.0", 18 | "react-dom": "^19.0.0", 19 | "react-router": "^7.4.0", 20 | "sonner": "^2.0.2", 21 | "tailwindcss": "^4.0.17" 22 | }, 23 | "devDependencies": { 24 | "@eslint/js": "^9.21.0", 25 | "@tanstack/react-query-devtools": "^5.69.0", 26 | "@types/react": "^19.0.10", 27 | "@types/react-dom": "^19.0.4", 28 | "@vitejs/plugin-react": "^4.3.4", 29 | "eslint": "^9.21.0", 30 | "eslint-plugin-react-hooks": "^5.1.0", 31 | "eslint-plugin-react-refresh": "^0.4.19", 32 | "globals": "^15.15.0", 33 | "typescript": "~5.7.2", 34 | "typescript-eslint": "^8.24.1", 35 | "vite": "^6.2.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ 3 | { 4 | "name": "sdgsg", 5 | "description": "dsgsg", 6 | "price": 43453, 7 | "stock": 34636, 8 | "category": "34636", 9 | "id": "p2", 10 | "createdAt": "2025-03-28T09:37:51.906Z", 11 | "updatedAt": "2025-03-28T09:37:51.906Z" 12 | }, 13 | { 14 | "name": "dsgsgs", 15 | "description": "sdgs", 16 | "price": 0, 17 | "stock": 0, 18 | "category": "dgsg", 19 | "id": "p2", 20 | "createdAt": "2025-03-28T09:39:20.334Z", 21 | "updatedAt": "2025-03-28T09:39:20.334Z" 22 | } 23 | ], 24 | "orders": [ 25 | { 26 | "id": "o1", 27 | "userId": "u1", 28 | "products": [ 29 | { 30 | "productId": "p1", 31 | "quantity": 1, 32 | "price": 799.99 33 | }, 34 | { 35 | "productId": "p3", 36 | "quantity": 1, 37 | "price": 199.99 38 | } 39 | ], 40 | "total": 999.98, 41 | "status": "DELIVERED", 42 | "createdAt": "2023-02-01T15:30:00Z", 43 | "updatedAt": "2023-02-05T10:15:00Z" 44 | }, 45 | { 46 | "id": "o2", 47 | "userId": "u2", 48 | "products": [ 49 | { 50 | "productId": "p2", 51 | "quantity": 1, 52 | "price": 1299.99 53 | } 54 | ], 55 | "total": 1299.99, 56 | "status": "PROCESSING", 57 | "createdAt": "2023-02-10T09:45:00Z", 58 | "updatedAt": "2023-02-10T09:45:00Z" 59 | }, 60 | { 61 | "id": "o3", 62 | "userId": "u1", 63 | "products": [ 64 | { 65 | "productId": "p4", 66 | "quantity": 2, 67 | "price": 19.99 68 | }, 69 | { 70 | "productId": "p5", 71 | "quantity": 1, 72 | "price": 49.99 73 | } 74 | ], 75 | "total": 89.97, 76 | "status": "PENDING", 77 | "createdAt": "2023-02-15T14:20:00Z", 78 | "updatedAt": "2023-02-15T14:20:00Z" 79 | } 80 | ], 81 | "users": [ 82 | { 83 | "id": "u1", 84 | "email": "john.doe@example.com", 85 | "firstName": "John", 86 | "lastName": "Doe", 87 | "role": "USER", 88 | "createdAt": "2023-01-01T10:00:00Z", 89 | "updatedAt": "2023-01-01T10:00:00Z" 90 | }, 91 | { 92 | "id": "u2", 93 | "email": "jane.smith@example.com", 94 | "firstName": "Jane", 95 | "lastName": "Smith", 96 | "role": "USER", 97 | "createdAt": "2023-01-02T11:30:00Z", 98 | "updatedAt": "2023-01-02T11:30:00Z" 99 | }, 100 | { 101 | "id": "u3", 102 | "email": "admin@example.com", 103 | "firstName": "Admin", 104 | "lastName": "User", 105 | "role": "ADMIN", 106 | "createdAt": "2023-01-03T09:15:00Z", 107 | "updatedAt": "2023-01-03T09:15:00Z" 108 | } 109 | ] 110 | } -------------------------------------------------------------------------------- /server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "server", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "cors": "^2.8.5", 13 | "express": "^4.21.2" 14 | } 15 | }, 16 | "node_modules/accepts": { 17 | "version": "1.3.8", 18 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 19 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 20 | "license": "MIT", 21 | "dependencies": { 22 | "mime-types": "~2.1.34", 23 | "negotiator": "0.6.3" 24 | }, 25 | "engines": { 26 | "node": ">= 0.6" 27 | } 28 | }, 29 | "node_modules/array-flatten": { 30 | "version": "1.1.1", 31 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 32 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", 33 | "license": "MIT" 34 | }, 35 | "node_modules/body-parser": { 36 | "version": "1.20.3", 37 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", 38 | "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", 39 | "license": "MIT", 40 | "dependencies": { 41 | "bytes": "3.1.2", 42 | "content-type": "~1.0.5", 43 | "debug": "2.6.9", 44 | "depd": "2.0.0", 45 | "destroy": "1.2.0", 46 | "http-errors": "2.0.0", 47 | "iconv-lite": "0.4.24", 48 | "on-finished": "2.4.1", 49 | "qs": "6.13.0", 50 | "raw-body": "2.5.2", 51 | "type-is": "~1.6.18", 52 | "unpipe": "1.0.0" 53 | }, 54 | "engines": { 55 | "node": ">= 0.8", 56 | "npm": "1.2.8000 || >= 1.4.16" 57 | } 58 | }, 59 | "node_modules/bytes": { 60 | "version": "3.1.2", 61 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 62 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 63 | "license": "MIT", 64 | "engines": { 65 | "node": ">= 0.8" 66 | } 67 | }, 68 | "node_modules/call-bind-apply-helpers": { 69 | "version": "1.0.2", 70 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 71 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 72 | "license": "MIT", 73 | "dependencies": { 74 | "es-errors": "^1.3.0", 75 | "function-bind": "^1.1.2" 76 | }, 77 | "engines": { 78 | "node": ">= 0.4" 79 | } 80 | }, 81 | "node_modules/call-bound": { 82 | "version": "1.0.4", 83 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 84 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 85 | "license": "MIT", 86 | "dependencies": { 87 | "call-bind-apply-helpers": "^1.0.2", 88 | "get-intrinsic": "^1.3.0" 89 | }, 90 | "engines": { 91 | "node": ">= 0.4" 92 | }, 93 | "funding": { 94 | "url": "https://github.com/sponsors/ljharb" 95 | } 96 | }, 97 | "node_modules/content-disposition": { 98 | "version": "0.5.4", 99 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 100 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 101 | "license": "MIT", 102 | "dependencies": { 103 | "safe-buffer": "5.2.1" 104 | }, 105 | "engines": { 106 | "node": ">= 0.6" 107 | } 108 | }, 109 | "node_modules/content-type": { 110 | "version": "1.0.5", 111 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 112 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 113 | "license": "MIT", 114 | "engines": { 115 | "node": ">= 0.6" 116 | } 117 | }, 118 | "node_modules/cookie": { 119 | "version": "0.7.1", 120 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", 121 | "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", 122 | "license": "MIT", 123 | "engines": { 124 | "node": ">= 0.6" 125 | } 126 | }, 127 | "node_modules/cookie-signature": { 128 | "version": "1.0.6", 129 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 130 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", 131 | "license": "MIT" 132 | }, 133 | "node_modules/cors": { 134 | "version": "2.8.5", 135 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 136 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 137 | "license": "MIT", 138 | "dependencies": { 139 | "object-assign": "^4", 140 | "vary": "^1" 141 | }, 142 | "engines": { 143 | "node": ">= 0.10" 144 | } 145 | }, 146 | "node_modules/debug": { 147 | "version": "2.6.9", 148 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 149 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 150 | "license": "MIT", 151 | "dependencies": { 152 | "ms": "2.0.0" 153 | } 154 | }, 155 | "node_modules/depd": { 156 | "version": "2.0.0", 157 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 158 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 159 | "license": "MIT", 160 | "engines": { 161 | "node": ">= 0.8" 162 | } 163 | }, 164 | "node_modules/destroy": { 165 | "version": "1.2.0", 166 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 167 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 168 | "license": "MIT", 169 | "engines": { 170 | "node": ">= 0.8", 171 | "npm": "1.2.8000 || >= 1.4.16" 172 | } 173 | }, 174 | "node_modules/dunder-proto": { 175 | "version": "1.0.1", 176 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 177 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 178 | "license": "MIT", 179 | "dependencies": { 180 | "call-bind-apply-helpers": "^1.0.1", 181 | "es-errors": "^1.3.0", 182 | "gopd": "^1.2.0" 183 | }, 184 | "engines": { 185 | "node": ">= 0.4" 186 | } 187 | }, 188 | "node_modules/ee-first": { 189 | "version": "1.1.1", 190 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 191 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 192 | "license": "MIT" 193 | }, 194 | "node_modules/encodeurl": { 195 | "version": "2.0.0", 196 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 197 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 198 | "license": "MIT", 199 | "engines": { 200 | "node": ">= 0.8" 201 | } 202 | }, 203 | "node_modules/es-define-property": { 204 | "version": "1.0.1", 205 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 206 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 207 | "license": "MIT", 208 | "engines": { 209 | "node": ">= 0.4" 210 | } 211 | }, 212 | "node_modules/es-errors": { 213 | "version": "1.3.0", 214 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 215 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 216 | "license": "MIT", 217 | "engines": { 218 | "node": ">= 0.4" 219 | } 220 | }, 221 | "node_modules/es-object-atoms": { 222 | "version": "1.1.1", 223 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 224 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 225 | "license": "MIT", 226 | "dependencies": { 227 | "es-errors": "^1.3.0" 228 | }, 229 | "engines": { 230 | "node": ">= 0.4" 231 | } 232 | }, 233 | "node_modules/escape-html": { 234 | "version": "1.0.3", 235 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 236 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 237 | "license": "MIT" 238 | }, 239 | "node_modules/etag": { 240 | "version": "1.8.1", 241 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 242 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 243 | "license": "MIT", 244 | "engines": { 245 | "node": ">= 0.6" 246 | } 247 | }, 248 | "node_modules/express": { 249 | "version": "4.21.2", 250 | "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", 251 | "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", 252 | "license": "MIT", 253 | "dependencies": { 254 | "accepts": "~1.3.8", 255 | "array-flatten": "1.1.1", 256 | "body-parser": "1.20.3", 257 | "content-disposition": "0.5.4", 258 | "content-type": "~1.0.4", 259 | "cookie": "0.7.1", 260 | "cookie-signature": "1.0.6", 261 | "debug": "2.6.9", 262 | "depd": "2.0.0", 263 | "encodeurl": "~2.0.0", 264 | "escape-html": "~1.0.3", 265 | "etag": "~1.8.1", 266 | "finalhandler": "1.3.1", 267 | "fresh": "0.5.2", 268 | "http-errors": "2.0.0", 269 | "merge-descriptors": "1.0.3", 270 | "methods": "~1.1.2", 271 | "on-finished": "2.4.1", 272 | "parseurl": "~1.3.3", 273 | "path-to-regexp": "0.1.12", 274 | "proxy-addr": "~2.0.7", 275 | "qs": "6.13.0", 276 | "range-parser": "~1.2.1", 277 | "safe-buffer": "5.2.1", 278 | "send": "0.19.0", 279 | "serve-static": "1.16.2", 280 | "setprototypeof": "1.2.0", 281 | "statuses": "2.0.1", 282 | "type-is": "~1.6.18", 283 | "utils-merge": "1.0.1", 284 | "vary": "~1.1.2" 285 | }, 286 | "engines": { 287 | "node": ">= 0.10.0" 288 | }, 289 | "funding": { 290 | "type": "opencollective", 291 | "url": "https://opencollective.com/express" 292 | } 293 | }, 294 | "node_modules/finalhandler": { 295 | "version": "1.3.1", 296 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", 297 | "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", 298 | "license": "MIT", 299 | "dependencies": { 300 | "debug": "2.6.9", 301 | "encodeurl": "~2.0.0", 302 | "escape-html": "~1.0.3", 303 | "on-finished": "2.4.1", 304 | "parseurl": "~1.3.3", 305 | "statuses": "2.0.1", 306 | "unpipe": "~1.0.0" 307 | }, 308 | "engines": { 309 | "node": ">= 0.8" 310 | } 311 | }, 312 | "node_modules/forwarded": { 313 | "version": "0.2.0", 314 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 315 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 316 | "license": "MIT", 317 | "engines": { 318 | "node": ">= 0.6" 319 | } 320 | }, 321 | "node_modules/fresh": { 322 | "version": "0.5.2", 323 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 324 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 325 | "license": "MIT", 326 | "engines": { 327 | "node": ">= 0.6" 328 | } 329 | }, 330 | "node_modules/function-bind": { 331 | "version": "1.1.2", 332 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 333 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 334 | "license": "MIT", 335 | "funding": { 336 | "url": "https://github.com/sponsors/ljharb" 337 | } 338 | }, 339 | "node_modules/get-intrinsic": { 340 | "version": "1.3.0", 341 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 342 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 343 | "license": "MIT", 344 | "dependencies": { 345 | "call-bind-apply-helpers": "^1.0.2", 346 | "es-define-property": "^1.0.1", 347 | "es-errors": "^1.3.0", 348 | "es-object-atoms": "^1.1.1", 349 | "function-bind": "^1.1.2", 350 | "get-proto": "^1.0.1", 351 | "gopd": "^1.2.0", 352 | "has-symbols": "^1.1.0", 353 | "hasown": "^2.0.2", 354 | "math-intrinsics": "^1.1.0" 355 | }, 356 | "engines": { 357 | "node": ">= 0.4" 358 | }, 359 | "funding": { 360 | "url": "https://github.com/sponsors/ljharb" 361 | } 362 | }, 363 | "node_modules/get-proto": { 364 | "version": "1.0.1", 365 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 366 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 367 | "license": "MIT", 368 | "dependencies": { 369 | "dunder-proto": "^1.0.1", 370 | "es-object-atoms": "^1.0.0" 371 | }, 372 | "engines": { 373 | "node": ">= 0.4" 374 | } 375 | }, 376 | "node_modules/gopd": { 377 | "version": "1.2.0", 378 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 379 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 380 | "license": "MIT", 381 | "engines": { 382 | "node": ">= 0.4" 383 | }, 384 | "funding": { 385 | "url": "https://github.com/sponsors/ljharb" 386 | } 387 | }, 388 | "node_modules/has-symbols": { 389 | "version": "1.1.0", 390 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 391 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 392 | "license": "MIT", 393 | "engines": { 394 | "node": ">= 0.4" 395 | }, 396 | "funding": { 397 | "url": "https://github.com/sponsors/ljharb" 398 | } 399 | }, 400 | "node_modules/hasown": { 401 | "version": "2.0.2", 402 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 403 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 404 | "license": "MIT", 405 | "dependencies": { 406 | "function-bind": "^1.1.2" 407 | }, 408 | "engines": { 409 | "node": ">= 0.4" 410 | } 411 | }, 412 | "node_modules/http-errors": { 413 | "version": "2.0.0", 414 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 415 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 416 | "license": "MIT", 417 | "dependencies": { 418 | "depd": "2.0.0", 419 | "inherits": "2.0.4", 420 | "setprototypeof": "1.2.0", 421 | "statuses": "2.0.1", 422 | "toidentifier": "1.0.1" 423 | }, 424 | "engines": { 425 | "node": ">= 0.8" 426 | } 427 | }, 428 | "node_modules/iconv-lite": { 429 | "version": "0.4.24", 430 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 431 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 432 | "license": "MIT", 433 | "dependencies": { 434 | "safer-buffer": ">= 2.1.2 < 3" 435 | }, 436 | "engines": { 437 | "node": ">=0.10.0" 438 | } 439 | }, 440 | "node_modules/inherits": { 441 | "version": "2.0.4", 442 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 443 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 444 | "license": "ISC" 445 | }, 446 | "node_modules/ipaddr.js": { 447 | "version": "1.9.1", 448 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 449 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 450 | "license": "MIT", 451 | "engines": { 452 | "node": ">= 0.10" 453 | } 454 | }, 455 | "node_modules/math-intrinsics": { 456 | "version": "1.1.0", 457 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 458 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 459 | "license": "MIT", 460 | "engines": { 461 | "node": ">= 0.4" 462 | } 463 | }, 464 | "node_modules/media-typer": { 465 | "version": "0.3.0", 466 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 467 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 468 | "license": "MIT", 469 | "engines": { 470 | "node": ">= 0.6" 471 | } 472 | }, 473 | "node_modules/merge-descriptors": { 474 | "version": "1.0.3", 475 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", 476 | "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", 477 | "license": "MIT", 478 | "funding": { 479 | "url": "https://github.com/sponsors/sindresorhus" 480 | } 481 | }, 482 | "node_modules/methods": { 483 | "version": "1.1.2", 484 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 485 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 486 | "license": "MIT", 487 | "engines": { 488 | "node": ">= 0.6" 489 | } 490 | }, 491 | "node_modules/mime": { 492 | "version": "1.6.0", 493 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 494 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 495 | "license": "MIT", 496 | "bin": { 497 | "mime": "cli.js" 498 | }, 499 | "engines": { 500 | "node": ">=4" 501 | } 502 | }, 503 | "node_modules/mime-db": { 504 | "version": "1.52.0", 505 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 506 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 507 | "license": "MIT", 508 | "engines": { 509 | "node": ">= 0.6" 510 | } 511 | }, 512 | "node_modules/mime-types": { 513 | "version": "2.1.35", 514 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 515 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 516 | "license": "MIT", 517 | "dependencies": { 518 | "mime-db": "1.52.0" 519 | }, 520 | "engines": { 521 | "node": ">= 0.6" 522 | } 523 | }, 524 | "node_modules/ms": { 525 | "version": "2.0.0", 526 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 527 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 528 | "license": "MIT" 529 | }, 530 | "node_modules/negotiator": { 531 | "version": "0.6.3", 532 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 533 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 534 | "license": "MIT", 535 | "engines": { 536 | "node": ">= 0.6" 537 | } 538 | }, 539 | "node_modules/object-assign": { 540 | "version": "4.1.1", 541 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 542 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 543 | "license": "MIT", 544 | "engines": { 545 | "node": ">=0.10.0" 546 | } 547 | }, 548 | "node_modules/object-inspect": { 549 | "version": "1.13.4", 550 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 551 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 552 | "license": "MIT", 553 | "engines": { 554 | "node": ">= 0.4" 555 | }, 556 | "funding": { 557 | "url": "https://github.com/sponsors/ljharb" 558 | } 559 | }, 560 | "node_modules/on-finished": { 561 | "version": "2.4.1", 562 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 563 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 564 | "license": "MIT", 565 | "dependencies": { 566 | "ee-first": "1.1.1" 567 | }, 568 | "engines": { 569 | "node": ">= 0.8" 570 | } 571 | }, 572 | "node_modules/parseurl": { 573 | "version": "1.3.3", 574 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 575 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 576 | "license": "MIT", 577 | "engines": { 578 | "node": ">= 0.8" 579 | } 580 | }, 581 | "node_modules/path-to-regexp": { 582 | "version": "0.1.12", 583 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", 584 | "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", 585 | "license": "MIT" 586 | }, 587 | "node_modules/proxy-addr": { 588 | "version": "2.0.7", 589 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 590 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 591 | "license": "MIT", 592 | "dependencies": { 593 | "forwarded": "0.2.0", 594 | "ipaddr.js": "1.9.1" 595 | }, 596 | "engines": { 597 | "node": ">= 0.10" 598 | } 599 | }, 600 | "node_modules/qs": { 601 | "version": "6.13.0", 602 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 603 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 604 | "license": "BSD-3-Clause", 605 | "dependencies": { 606 | "side-channel": "^1.0.6" 607 | }, 608 | "engines": { 609 | "node": ">=0.6" 610 | }, 611 | "funding": { 612 | "url": "https://github.com/sponsors/ljharb" 613 | } 614 | }, 615 | "node_modules/range-parser": { 616 | "version": "1.2.1", 617 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 618 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 619 | "license": "MIT", 620 | "engines": { 621 | "node": ">= 0.6" 622 | } 623 | }, 624 | "node_modules/raw-body": { 625 | "version": "2.5.2", 626 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 627 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 628 | "license": "MIT", 629 | "dependencies": { 630 | "bytes": "3.1.2", 631 | "http-errors": "2.0.0", 632 | "iconv-lite": "0.4.24", 633 | "unpipe": "1.0.0" 634 | }, 635 | "engines": { 636 | "node": ">= 0.8" 637 | } 638 | }, 639 | "node_modules/safe-buffer": { 640 | "version": "5.2.1", 641 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 642 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 643 | "funding": [ 644 | { 645 | "type": "github", 646 | "url": "https://github.com/sponsors/feross" 647 | }, 648 | { 649 | "type": "patreon", 650 | "url": "https://www.patreon.com/feross" 651 | }, 652 | { 653 | "type": "consulting", 654 | "url": "https://feross.org/support" 655 | } 656 | ], 657 | "license": "MIT" 658 | }, 659 | "node_modules/safer-buffer": { 660 | "version": "2.1.2", 661 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 662 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 663 | "license": "MIT" 664 | }, 665 | "node_modules/send": { 666 | "version": "0.19.0", 667 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", 668 | "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", 669 | "license": "MIT", 670 | "dependencies": { 671 | "debug": "2.6.9", 672 | "depd": "2.0.0", 673 | "destroy": "1.2.0", 674 | "encodeurl": "~1.0.2", 675 | "escape-html": "~1.0.3", 676 | "etag": "~1.8.1", 677 | "fresh": "0.5.2", 678 | "http-errors": "2.0.0", 679 | "mime": "1.6.0", 680 | "ms": "2.1.3", 681 | "on-finished": "2.4.1", 682 | "range-parser": "~1.2.1", 683 | "statuses": "2.0.1" 684 | }, 685 | "engines": { 686 | "node": ">= 0.8.0" 687 | } 688 | }, 689 | "node_modules/send/node_modules/encodeurl": { 690 | "version": "1.0.2", 691 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 692 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 693 | "license": "MIT", 694 | "engines": { 695 | "node": ">= 0.8" 696 | } 697 | }, 698 | "node_modules/send/node_modules/ms": { 699 | "version": "2.1.3", 700 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 701 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 702 | "license": "MIT" 703 | }, 704 | "node_modules/serve-static": { 705 | "version": "1.16.2", 706 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", 707 | "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", 708 | "license": "MIT", 709 | "dependencies": { 710 | "encodeurl": "~2.0.0", 711 | "escape-html": "~1.0.3", 712 | "parseurl": "~1.3.3", 713 | "send": "0.19.0" 714 | }, 715 | "engines": { 716 | "node": ">= 0.8.0" 717 | } 718 | }, 719 | "node_modules/setprototypeof": { 720 | "version": "1.2.0", 721 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 722 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 723 | "license": "ISC" 724 | }, 725 | "node_modules/side-channel": { 726 | "version": "1.1.0", 727 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 728 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 729 | "license": "MIT", 730 | "dependencies": { 731 | "es-errors": "^1.3.0", 732 | "object-inspect": "^1.13.3", 733 | "side-channel-list": "^1.0.0", 734 | "side-channel-map": "^1.0.1", 735 | "side-channel-weakmap": "^1.0.2" 736 | }, 737 | "engines": { 738 | "node": ">= 0.4" 739 | }, 740 | "funding": { 741 | "url": "https://github.com/sponsors/ljharb" 742 | } 743 | }, 744 | "node_modules/side-channel-list": { 745 | "version": "1.0.0", 746 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 747 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 748 | "license": "MIT", 749 | "dependencies": { 750 | "es-errors": "^1.3.0", 751 | "object-inspect": "^1.13.3" 752 | }, 753 | "engines": { 754 | "node": ">= 0.4" 755 | }, 756 | "funding": { 757 | "url": "https://github.com/sponsors/ljharb" 758 | } 759 | }, 760 | "node_modules/side-channel-map": { 761 | "version": "1.0.1", 762 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 763 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 764 | "license": "MIT", 765 | "dependencies": { 766 | "call-bound": "^1.0.2", 767 | "es-errors": "^1.3.0", 768 | "get-intrinsic": "^1.2.5", 769 | "object-inspect": "^1.13.3" 770 | }, 771 | "engines": { 772 | "node": ">= 0.4" 773 | }, 774 | "funding": { 775 | "url": "https://github.com/sponsors/ljharb" 776 | } 777 | }, 778 | "node_modules/side-channel-weakmap": { 779 | "version": "1.0.2", 780 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 781 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 782 | "license": "MIT", 783 | "dependencies": { 784 | "call-bound": "^1.0.2", 785 | "es-errors": "^1.3.0", 786 | "get-intrinsic": "^1.2.5", 787 | "object-inspect": "^1.13.3", 788 | "side-channel-map": "^1.0.1" 789 | }, 790 | "engines": { 791 | "node": ">= 0.4" 792 | }, 793 | "funding": { 794 | "url": "https://github.com/sponsors/ljharb" 795 | } 796 | }, 797 | "node_modules/statuses": { 798 | "version": "2.0.1", 799 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 800 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 801 | "license": "MIT", 802 | "engines": { 803 | "node": ">= 0.8" 804 | } 805 | }, 806 | "node_modules/toidentifier": { 807 | "version": "1.0.1", 808 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 809 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 810 | "license": "MIT", 811 | "engines": { 812 | "node": ">=0.6" 813 | } 814 | }, 815 | "node_modules/type-is": { 816 | "version": "1.6.18", 817 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 818 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 819 | "license": "MIT", 820 | "dependencies": { 821 | "media-typer": "0.3.0", 822 | "mime-types": "~2.1.24" 823 | }, 824 | "engines": { 825 | "node": ">= 0.6" 826 | } 827 | }, 828 | "node_modules/unpipe": { 829 | "version": "1.0.0", 830 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 831 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 832 | "license": "MIT", 833 | "engines": { 834 | "node": ">= 0.8" 835 | } 836 | }, 837 | "node_modules/utils-merge": { 838 | "version": "1.0.1", 839 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 840 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 841 | "license": "MIT", 842 | "engines": { 843 | "node": ">= 0.4.0" 844 | } 845 | }, 846 | "node_modules/vary": { 847 | "version": "1.1.2", 848 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 849 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 850 | "license": "MIT", 851 | "engines": { 852 | "node": ">= 0.8" 853 | } 854 | } 855 | } 856 | } 857 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "main": "server.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "start": "node server.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "description": "", 13 | "dependencies": { 14 | "cors": "^2.8.5", 15 | "express": "^4.21.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const cors = require("cors"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const app = express(); 6 | const PORT = process.env.PORT || 3001; 7 | 8 | // Middleware 9 | app.use(cors()); 10 | app.use(express.json()); 11 | 12 | // Read the database file 13 | const dbPath = path.join(__dirname, "db.json"); 14 | let database = JSON.parse(fs.readFileSync(dbPath, "utf8")); 15 | 16 | // Helper function to save changes to the database file 17 | const saveDatabase = () => { 18 | fs.writeFileSync(dbPath, JSON.stringify(database, null, 2), "utf8"); 19 | }; 20 | 21 | // Helper function to paginate results 22 | const paginateResults = (data, page = 1, limit = 10) => { 23 | const startIndex = (page - 1) * limit; 24 | const endIndex = page * limit; 25 | const paginatedData = data.slice(startIndex, endIndex); 26 | 27 | return { 28 | data: paginatedData, 29 | total: data.length, 30 | page: parseInt(page), 31 | limit: parseInt(limit), 32 | hasMore: endIndex < data.length, 33 | }; 34 | }; 35 | 36 | // PRODUCT ENDPOINTS 37 | // Get all products with optional filtering and pagination 38 | app.get("/products", (req, res) => { 39 | const { 40 | category, 41 | minPrice, 42 | maxPrice, 43 | search, 44 | page = 1, 45 | limit = 10, 46 | } = req.query; 47 | let filteredProducts = [...database.products]; 48 | 49 | if (category) { 50 | filteredProducts = filteredProducts.filter( 51 | (product) => product.category === category 52 | ); 53 | } 54 | 55 | if (minPrice) { 56 | filteredProducts = filteredProducts.filter( 57 | (product) => product.price >= parseFloat(minPrice) 58 | ); 59 | } 60 | 61 | if (maxPrice) { 62 | filteredProducts = filteredProducts.filter( 63 | (product) => product.price <= parseFloat(maxPrice) 64 | ); 65 | } 66 | 67 | if (search) { 68 | const searchLower = search.toLowerCase(); 69 | filteredProducts = filteredProducts.filter( 70 | (product) => 71 | product.name.toLowerCase().includes(searchLower) || 72 | product.description.toLowerCase().includes(searchLower) 73 | ); 74 | } 75 | 76 | // Return paginated response 77 | res.json(paginateResults(filteredProducts, page, limit)); 78 | }); 79 | 80 | // Get a single product by ID 81 | app.get("/products/:id", (req, res) => { 82 | const product = database.products.find((p) => p.id === req.params.id); 83 | if (product) { 84 | res.json(product); 85 | } else { 86 | res.status(404).json({ 87 | message: "Product not found", 88 | code: "PRODUCT_NOT_FOUND", 89 | status: 404, 90 | }); 91 | } 92 | }); 93 | 94 | // Create a new product 95 | app.post("/products", (req, res) => { 96 | const newProduct = { 97 | ...req.body, 98 | id: `p${database.products.length + 1}`, 99 | createdAt: new Date().toISOString(), 100 | updatedAt: new Date().toISOString(), 101 | }; 102 | 103 | database.products.push(newProduct); 104 | saveDatabase(); 105 | res.status(201).json(newProduct); 106 | }); 107 | 108 | // Update an existing product 109 | app.put("/products/:id", (req, res) => { 110 | const index = database.products.findIndex((p) => p.id === req.params.id); 111 | if (index !== -1) { 112 | database.products[index] = { 113 | ...database.products[index], 114 | ...req.body, 115 | updatedAt: new Date().toISOString(), 116 | }; 117 | saveDatabase(); 118 | res.json(database.products[index]); 119 | } else { 120 | res.status(404).json({ 121 | message: "Product not found", 122 | code: "PRODUCT_NOT_FOUND", 123 | status: 404, 124 | }); 125 | } 126 | }); 127 | 128 | // Delete a product 129 | app.delete("/products/:id", (req, res) => { 130 | const index = database.products.findIndex((p) => p.id === req.params.id); 131 | if (index !== -1) { 132 | const deletedProduct = database.products[index]; 133 | database.products.splice(index, 1); 134 | saveDatabase(); 135 | res.json(deletedProduct); 136 | } else { 137 | res.status(404).json({ 138 | message: "Product not found", 139 | code: "PRODUCT_NOT_FOUND", 140 | status: 404, 141 | }); 142 | } 143 | }); 144 | 145 | // Get products by category with pagination 146 | app.get("/products/category/:category", (req, res) => { 147 | const { page = 1, limit = 10 } = req.query; 148 | const categoryProducts = database.products.filter( 149 | (p) => p.category === req.params.category 150 | ); 151 | res.json(paginateResults(categoryProducts, page, limit)); 152 | }); 153 | 154 | // ORDER ENDPOINTS 155 | // Get all orders with optional filtering and pagination 156 | app.get("/orders", (req, res) => { 157 | const { status, userId, page = 1, limit = 10 } = req.query; 158 | let filteredOrders = [...database.orders]; 159 | 160 | if (status) { 161 | filteredOrders = filteredOrders.filter((order) => order.status === status); 162 | } 163 | 164 | if (userId) { 165 | filteredOrders = filteredOrders.filter((order) => order.userId === userId); 166 | } 167 | 168 | res.json(paginateResults(filteredOrders, page, limit)); 169 | }); 170 | 171 | // Get a single order by ID 172 | app.get("/orders/:id", (req, res) => { 173 | const order = database.orders.find((o) => o.id === req.params.id); 174 | if (order) { 175 | res.json(order); 176 | } else { 177 | res.status(404).json({ 178 | message: "Order not found", 179 | code: "ORDER_NOT_FOUND", 180 | status: 404, 181 | }); 182 | } 183 | }); 184 | 185 | // Create a new order 186 | app.post("/orders", (req, res) => { 187 | const newOrder = { 188 | ...req.body, 189 | id: `o${database.orders.length + 1}`, 190 | createdAt: new Date().toISOString(), 191 | updatedAt: new Date().toISOString(), 192 | }; 193 | 194 | database.orders.push(newOrder); 195 | saveDatabase(); 196 | res.status(201).json(newOrder); 197 | }); 198 | 199 | // Update an existing order 200 | app.put("/orders/:id", (req, res) => { 201 | const index = database.orders.findIndex((o) => o.id === req.params.id); 202 | if (index !== -1) { 203 | database.orders[index] = { 204 | ...database.orders[index], 205 | ...req.body, 206 | updatedAt: new Date().toISOString(), 207 | }; 208 | saveDatabase(); 209 | res.json(database.orders[index]); 210 | } else { 211 | res.status(404).json({ 212 | message: "Order not found", 213 | code: "ORDER_NOT_FOUND", 214 | status: 404, 215 | }); 216 | } 217 | }); 218 | 219 | // Delete an order 220 | app.delete("/orders/:id", (req, res) => { 221 | const index = database.orders.findIndex((o) => o.id === req.params.id); 222 | if (index !== -1) { 223 | const deletedOrder = database.orders[index]; 224 | database.orders.splice(index, 1); 225 | saveDatabase(); 226 | res.json(deletedOrder); 227 | } else { 228 | res.status(404).json({ 229 | message: "Order not found", 230 | code: "ORDER_NOT_FOUND", 231 | status: 404, 232 | }); 233 | } 234 | }); 235 | 236 | // Get orders for a specific user with pagination 237 | app.get("/orders/user/:userId", (req, res) => { 238 | const { page = 1, limit = 10 } = req.query; 239 | const userOrders = database.orders.filter( 240 | (o) => o.userId === req.params.userId 241 | ); 242 | res.json(paginateResults(userOrders, page, limit)); 243 | }); 244 | 245 | // USER ENDPOINTS 246 | // Get all users with optional filtering and pagination 247 | app.get("/users", (req, res) => { 248 | const { role, page = 1, limit = 10 } = req.query; 249 | let filteredUsers = [...database.users]; 250 | 251 | if (role) { 252 | filteredUsers = filteredUsers.filter((user) => user.role === role); 253 | } 254 | 255 | res.json(paginateResults(filteredUsers, page, limit)); 256 | }); 257 | 258 | // Get a single user by ID 259 | app.get("/users/:id", (req, res) => { 260 | const user = database.users.find((u) => u.id === req.params.id); 261 | if (user) { 262 | res.json(user); 263 | } else { 264 | res.status(404).json({ 265 | message: "User not found", 266 | code: "USER_NOT_FOUND", 267 | status: 404, 268 | }); 269 | } 270 | }); 271 | 272 | // Create a new user 273 | app.post("/users", (req, res) => { 274 | const newUser = { 275 | ...req.body, 276 | id: `u${database.users.length + 1}`, 277 | createdAt: new Date().toISOString(), 278 | updatedAt: new Date().toISOString(), 279 | }; 280 | 281 | database.users.push(newUser); 282 | saveDatabase(); 283 | res.status(201).json(newUser); 284 | }); 285 | 286 | // Update an existing user 287 | app.put("/users/:id", (req, res) => { 288 | const index = database.users.findIndex((u) => u.id === req.params.id); 289 | if (index !== -1) { 290 | database.users[index] = { 291 | ...database.users[index], 292 | ...req.body, 293 | updatedAt: new Date().toISOString(), 294 | }; 295 | saveDatabase(); 296 | res.json(database.users[index]); 297 | } else { 298 | res.status(404).json({ 299 | message: "User not found", 300 | code: "USER_NOT_FOUND", 301 | status: 404, 302 | }); 303 | } 304 | }); 305 | 306 | // Delete a user 307 | app.delete("/users/:id", (req, res) => { 308 | const index = database.users.findIndex((u) => u.id === req.params.id); 309 | if (index !== -1) { 310 | const deletedUser = database.users[index]; 311 | database.users.splice(index, 1); 312 | saveDatabase(); 313 | res.json(deletedUser); 314 | } else { 315 | res.status(404).json({ 316 | message: "User not found", 317 | code: "USER_NOT_FOUND", 318 | status: 404, 319 | }); 320 | } 321 | }); 322 | 323 | // Get user profile (mock implementation) 324 | app.get("/users/profile", (req, res) => { 325 | // In a real app, this would use authentication to determine the current user 326 | // For this example, we'll just return the first user as a mock 327 | res.json(database.users[0]); 328 | }); 329 | 330 | // Start the server 331 | app.listen(PORT, () => { 332 | console.log(`Server is running on port ${PORT}`); 333 | }); 334 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { QueryClientProvider } from "@tanstack/react-query"; 3 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 4 | import { Toaster } from "sonner"; 5 | import { 6 | createBrowserRouter, 7 | Outlet, 8 | redirect, 9 | RouterProvider, 10 | } from "react-router"; 11 | 12 | import { queryClient } from "./api/queryClient"; 13 | 14 | // Layout components 15 | import Navbar from "./components/layout/Navbar"; 16 | import Sidebar from "./components/layout/Sidebar"; 17 | 18 | // Page components 19 | import ProductList from "./pages/products/components/ProductList"; 20 | import ProductDetail from "./pages/products/components/ProductDetail"; 21 | import OrderList from "./pages/orders/components/OrderList"; 22 | import OrderDetail from "./pages/orders/components/OrderDetail"; 23 | import UserList from "./pages/users/components/UserList"; 24 | import UserDetail from "./pages/users/components/UserDetail"; 25 | 26 | // Root layout component 27 | const RootLayout: React.FC = () => ( 28 |
29 | 30 |
31 | 32 |
33 | 34 |
35 |
36 | 37 |
38 | ); 39 | 40 | // Data router configuration 41 | const router = createBrowserRouter([ 42 | { 43 | path: "/", 44 | Component: RootLayout, 45 | children: [ 46 | // Default redirect to /products 47 | { 48 | index: true, 49 | loader: async () => redirect("/products"), 50 | }, 51 | // Product routes 52 | { path: "products", Component: ProductList }, 53 | { path: "products/:id", Component: ProductDetail }, 54 | // Order routes 55 | { path: "orders", Component: OrderList }, 56 | { path: "orders/:id", Component: OrderDetail }, 57 | // User routes 58 | { path: "users", Component: UserList }, 59 | { path: "users/:id", Component: UserDetail }, 60 | ], 61 | }, 62 | ]); 63 | 64 | const App: React.FC = () => ( 65 | 66 | 67 | {/* React Query Devtools */} 68 | {import.meta.env.VITE_ENABLE_QUERY_DEVTOOLS === "true" && ( 69 | 70 | )} 71 | 72 | ); 73 | 74 | export default App; 75 | -------------------------------------------------------------------------------- /src/api/axiosClient.ts: -------------------------------------------------------------------------------- 1 | import axios, { 2 | AxiosError, 3 | AxiosInstance, 4 | InternalAxiosRequestConfig, 5 | AxiosResponse, 6 | } from "axios"; 7 | import { ApiError } from "../types"; 8 | 9 | /** 10 | * Constants for authentication token storage 11 | */ 12 | const STORAGE_KEYS = { 13 | ACCESS_TOKEN: "access_token", 14 | REFRESH_TOKEN: "refresh_token", 15 | } as const; 16 | 17 | /** 18 | * Creates and configures the Axios instance 19 | * Uses IIFE pattern to ensure single instance 20 | */ 21 | export const axiosClient: AxiosInstance = (() => { 22 | return axios.create({ 23 | baseURL: import.meta.env.VITE_BASE_URL, 24 | headers: { 25 | Accept: "application/json", 26 | "Content-Type": "application/json", 27 | }, 28 | timeout: 10000, // 10 seconds 29 | }); 30 | })(); 31 | 32 | // Add custom _retry property to InternalAxiosRequestConfig 33 | declare module "axios" { 34 | export interface InternalAxiosRequestConfig { 35 | _retry?: boolean; 36 | } 37 | } 38 | 39 | /** 40 | * Request interceptor 41 | * - Adds authentication token 42 | * - Handles request configuration 43 | */ 44 | axiosClient.interceptors.request.use( 45 | (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => { 46 | // Add auth token if available 47 | const token = localStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN); 48 | if (token && config.headers) { 49 | config.headers.Authorization = `Bearer ${token}`; 50 | } 51 | return config; 52 | }, 53 | (error: AxiosError): Promise => { 54 | return Promise.reject(error); 55 | } 56 | ); 57 | 58 | interface ErrorResponse { 59 | message: string; 60 | code: string; 61 | } 62 | 63 | /** 64 | * Response interceptor 65 | * - Handles response data transformation 66 | * - Manages authentication errors 67 | * - Standardizes error handling 68 | */ 69 | axiosClient.interceptors.response.use( 70 | (response: AxiosResponse): AxiosResponse => { 71 | return response; 72 | }, 73 | async (error: AxiosError): Promise => { 74 | const originalRequest = error.config; 75 | 76 | // Handle 401 Unauthorized errors 77 | if ( 78 | error.response?.status === 401 && 79 | originalRequest && 80 | !originalRequest._retry 81 | ) { 82 | originalRequest._retry = true; 83 | 84 | try { 85 | // Attempt to refresh token 86 | const refreshToken = localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN); 87 | const response = await axios.post( 88 | `${import.meta.env.REACT_APP_BASE_URL}/auth/refresh`, 89 | { 90 | refreshToken, 91 | } 92 | ); 93 | 94 | const { accessToken, refreshToken: newRefreshToken } = response.data; 95 | 96 | // Update stored tokens 97 | localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken); 98 | localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, newRefreshToken); 99 | 100 | // Update authorization header 101 | if (originalRequest.headers) { 102 | originalRequest.headers.Authorization = `Bearer ${accessToken}`; 103 | } 104 | 105 | // Retry original request 106 | return axiosClient(originalRequest); 107 | } catch (refreshError) { 108 | // Handle refresh token failure 109 | localStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN); 110 | localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN); 111 | window.location.href = "/login"; 112 | return Promise.reject(refreshError); 113 | } 114 | } 115 | 116 | // Transform error response 117 | const apiError: ApiError = { 118 | message: error.response?.data?.message || "An unexpected error occurred", 119 | code: error.response?.data?.code || "UNKNOWN_ERROR", 120 | status: error.response?.status || 500, 121 | }; 122 | 123 | return Promise.reject(apiError); 124 | } 125 | ); 126 | 127 | export default axiosClient; 128 | -------------------------------------------------------------------------------- /src/api/endpoints.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * API endpoint configurations 3 | * Centralized endpoint management for the application 4 | */ 5 | 6 | /** 7 | * Product-related endpoints 8 | */ 9 | export const ProductEndpoints = { 10 | /** Get all products with optional filtering */ 11 | getAll: (params?: string) => `/products${params ? `?${params}` : ""}`, 12 | 13 | /** Get a single product by ID */ 14 | getById: (id: string) => `/products/${id}`, 15 | 16 | /** Create a new product */ 17 | create: () => "/products", 18 | 19 | /** Update an existing product */ 20 | update: (id: string) => `/products/${id}`, 21 | 22 | /** Delete a product */ 23 | delete: (id: string) => `/products/${id}`, 24 | 25 | /** Get products by category */ 26 | getByCategory: (category: string) => `/products/category/${category}`, 27 | } as const; 28 | 29 | /** 30 | * Order-related endpoints 31 | */ 32 | export const OrderEndpoints = { 33 | /** Get all orders with optional filtering */ 34 | getAll: (params?: string) => `/orders${params ? `?${params}` : ""}`, 35 | 36 | /** Get a single order by ID */ 37 | getById: (id: string) => `/orders/${id}`, 38 | 39 | /** Create a new order */ 40 | create: () => "/orders", 41 | 42 | /** Update an existing order */ 43 | update: (id: string) => `/orders/${id}`, 44 | 45 | /** Delete an order */ 46 | delete: (id: string) => `/orders/${id}`, 47 | 48 | /** Get orders for a specific user */ 49 | getByUser: (userId: string) => `/orders/user/${userId}`, 50 | } as const; 51 | 52 | /** 53 | * User-related endpoints 54 | */ 55 | export const UserEndpoints = { 56 | /** Get all users with optional filtering */ 57 | getAll: (params?: string) => `/users${params ? `?${params}` : ""}`, 58 | 59 | /** Get a single user by ID */ 60 | getById: (id: string) => `/users/${id}`, 61 | 62 | /** Create a new user */ 63 | create: () => "/users", 64 | 65 | /** Update an existing user */ 66 | update: (id: string) => `/users/${id}`, 67 | 68 | /** Delete a user */ 69 | delete: (id: string) => `/users/${id}`, 70 | 71 | /** Get user profile */ 72 | profile: () => "/users/profile", 73 | } as const; 74 | -------------------------------------------------------------------------------- /src/api/queryClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | QueryClient, 3 | QueryCache, 4 | MutationCache, 5 | QueryKey, 6 | } from "@tanstack/react-query"; 7 | import { toast } from "sonner"; 8 | 9 | interface QueryMeta { 10 | successMessage?: string; 11 | errorMessage?: string; 12 | invalidateQueries?: QueryKey | QueryKey[]; 13 | mutationId?: string; 14 | } 15 | 16 | /** 17 | * Configure and create the React Query client 18 | * Includes global configuration for queries and mutations 19 | */ 20 | export const queryClient = new QueryClient({ 21 | defaultOptions: { 22 | queries: { 23 | retry: 1, // Retry failed requests once 24 | refetchOnWindowFocus: false, // Disable automatic refetch on window focus 25 | staleTime: 30000, // Consider data fresh for 30 seconds 26 | }, 27 | }, 28 | 29 | /** 30 | * Global query cache configuration 31 | * Handles success and error states for all queries 32 | */ 33 | queryCache: new QueryCache({ 34 | onSuccess: (_data, query) => { 35 | // Handle successful queries 36 | const meta = query.meta as QueryMeta | undefined; 37 | if (meta?.successMessage) { 38 | toast.success(meta.successMessage); 39 | } 40 | }, 41 | onError: (error, query) => { 42 | // Handle query errors 43 | const meta = query.meta as QueryMeta | undefined; 44 | const errorMessage = meta?.errorMessage || (error as Error).message; 45 | toast.error(`${errorMessage}: ${(error as Error).message}`); 46 | 47 | // Log error for debugging 48 | console.error("Query Error:", { 49 | queryKey: query.queryKey, 50 | error, 51 | meta: query.meta, 52 | }); 53 | }, 54 | }), 55 | 56 | /** 57 | * Global mutation cache configuration 58 | * Handles success and error states for all mutations 59 | */ 60 | mutationCache: new MutationCache({ 61 | onSuccess: (_data, _variables, _context, mutation) => { 62 | // Handle successful mutations 63 | const meta = mutation.meta as QueryMeta | undefined; 64 | if (meta?.successMessage) { 65 | toast.success(meta.successMessage); 66 | } 67 | 68 | // Invalidate relevant queries if specified 69 | if (meta?.invalidateQueries) { 70 | const queriesToInvalidate = Array.isArray(meta.invalidateQueries) 71 | ? meta.invalidateQueries 72 | : [meta.invalidateQueries]; 73 | 74 | queriesToInvalidate.forEach((queryKey) => { 75 | queryClient.invalidateQueries({ queryKey }); 76 | }); 77 | } 78 | }, 79 | onError: (error, _variables, _context, mutation) => { 80 | // Handle mutation errors 81 | const meta = mutation.meta as QueryMeta | undefined; 82 | const errorMessage = meta?.errorMessage || "Operation failed"; 83 | toast.error(`${errorMessage}: ${(error as Error).message}`); 84 | 85 | // Log error for debugging 86 | console.error("Mutation Error:", { 87 | mutation: meta?.mutationId, 88 | error, 89 | }); 90 | }, 91 | }), 92 | }); 93 | 94 | /** 95 | * Query key factory 96 | * Provides consistent query keys across the application 97 | */ 98 | export const queryKeys = { 99 | products: { 100 | all: ["products"] as const, 101 | byId: (id: string) => ["products", id] as const, 102 | byCategory: (category: string) => 103 | ["products", "category", category] as const, 104 | }, 105 | orders: { 106 | all: ["orders"] as const, 107 | byId: (id: string) => ["orders", id] as const, 108 | byUser: (userId: string) => ["orders", "user", userId] as const, 109 | }, 110 | users: { 111 | all: ["users"] as const, 112 | byId: (id: string) => ["users", id] as const, 113 | }, 114 | }; 115 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/layout/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Link, useLocation } from "react-router"; 3 | 4 | const Navbar: React.FC = () => { 5 | const location = useLocation(); 6 | const [isMenuOpen, setIsMenuOpen] = useState(false); 7 | 8 | const toggleMenu = () => { 9 | setIsMenuOpen(!isMenuOpen); 10 | }; 11 | 12 | return ( 13 | 127 | ); 128 | }; 129 | 130 | export default Navbar; 131 | -------------------------------------------------------------------------------- /src/components/layout/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useLocation, useNavigate } from "react-router"; 3 | 4 | interface SidebarProps { 5 | isMobileOpen?: boolean; 6 | onMobileClose?: () => void; 7 | } 8 | 9 | const Sidebar: React.FC = ({ 10 | isMobileOpen = false, 11 | onMobileClose = () => {}, 12 | }) => { 13 | const location = useLocation(); 14 | const navigate = useNavigate(); 15 | const [isCollapsed, setIsCollapsed] = useState(false); 16 | const [isMobile, setIsMobile] = useState(false); 17 | 18 | // Get current section from URL 19 | const currentSection = location.pathname.split("/")[1]; 20 | 21 | // Check if we're on mobile 22 | useEffect(() => { 23 | const checkIfMobile = () => { 24 | setIsMobile(window.innerWidth < 768); 25 | // Auto-collapse on small screens 26 | if (window.innerWidth < 768) { 27 | setIsCollapsed(true); 28 | } 29 | }; 30 | 31 | checkIfMobile(); 32 | window.addEventListener("resize", checkIfMobile); 33 | 34 | return () => { 35 | window.removeEventListener("resize", checkIfMobile); 36 | }; 37 | }, []); 38 | 39 | // Menu items with added icons 40 | const menuItems = { 41 | products: [ 42 | { 43 | label: "All Products", 44 | path: "/products", 45 | icon: ( 46 | 53 | 59 | 60 | ), 61 | }, 62 | { 63 | label: "Add Product", 64 | path: "/products/new", 65 | icon: ( 66 | 73 | 79 | 80 | ), 81 | }, 82 | { 83 | label: "Categories", 84 | path: "/products/categories", 85 | icon: ( 86 | 93 | 99 | 100 | ), 101 | }, 102 | ], 103 | orders: [ 104 | { 105 | label: "All Orders", 106 | path: "/orders", 107 | icon: ( 108 | 115 | 121 | 122 | ), 123 | }, 124 | { 125 | label: "Pending Orders", 126 | path: "/orders?status=pending", 127 | icon: ( 128 | 135 | 141 | 142 | ), 143 | }, 144 | { 145 | label: "Completed Orders", 146 | path: "/orders?status=completed", 147 | icon: ( 148 | 155 | 161 | 162 | ), 163 | }, 164 | ], 165 | users: [ 166 | { 167 | label: "All Users", 168 | path: "/users", 169 | icon: ( 170 | 177 | 183 | 184 | ), 185 | }, 186 | { 187 | label: "Add User", 188 | path: "/users/new", 189 | icon: ( 190 | 197 | 203 | 204 | ), 205 | }, 206 | { 207 | label: "User Roles", 208 | path: "/users/roles", 209 | icon: ( 210 | 217 | 223 | 229 | 230 | ), 231 | }, 232 | ], 233 | }; 234 | 235 | const getCurrentMenu = () => { 236 | switch (currentSection) { 237 | case "products": 238 | return menuItems.products; 239 | case "orders": 240 | return menuItems.orders; 241 | case "users": 242 | return menuItems.users; 243 | default: 244 | return []; 245 | } 246 | }; 247 | 248 | const handleNavigation = (path: string) => { 249 | navigate(path); 250 | if (isMobile && onMobileClose) { 251 | onMobileClose(); 252 | } 253 | }; 254 | 255 | // Sidebar classes based on state - modified to always show at least icons 256 | const sidebarClasses = ` 257 | ${isMobile ? "fixed left-0 top-0 bottom-0 z-40" : "relative"} 258 | ${!isMobileOpen && isMobile ? "w-16" : ""} 259 | ${isMobileOpen && isMobile ? "w-64" : ""} 260 | ${!isMobile && isCollapsed ? "w-16" : "w-64"} 261 | bg-white shadow-md transition-all duration-300 h-full 262 | `; 263 | 264 | return ( 265 | <> 266 | {/* Backdrop for mobile - only when expanded */} 267 | {isMobile && isMobileOpen && ( 268 |
272 | )} 273 | 274 | 345 | 346 | ); 347 | }; 348 | 349 | export default Sidebar; 350 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.tsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /src/pages/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hassan-kamel/axios-react-query-example/070e3ba929b38009f07d89f3dcecd754863e2c75/src/pages/index.ts -------------------------------------------------------------------------------- /src/pages/orders/components/OrderCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Order, OrderStatus } from "../../../types"; 3 | 4 | interface OrderCardProps { 5 | order: Order; 6 | onDelete: (id: string, e: React.MouseEvent) => Promise; 7 | onClick: (id: string) => void; 8 | isDeleting: boolean; 9 | } 10 | 11 | const OrderCard: React.FC = ({ 12 | order, 13 | onDelete, 14 | onClick, 15 | isDeleting, 16 | }) => { 17 | return ( 18 |
onClick(order.id)} 21 | > 22 |

23 | Order #{order.id ? order.id.slice(-6) : "N/A"} 24 |

25 |
26 | Customer: {order.customerName || "Unknown"} 27 | 36 | {(order.status || OrderStatus.PENDING).toString()} 37 | 38 |
39 |
40 |

Items: {order.items?.length || 0}

41 |

Total: ${(order.total || 0).toFixed(2)}

42 |
43 |
44 | 45 | {order.orderDate 46 | ? new Date(order.orderDate).toLocaleDateString() 47 | : "No date"} 48 | 49 | 59 |
60 |
61 | ); 62 | }; 63 | 64 | export default OrderCard; 65 | -------------------------------------------------------------------------------- /src/pages/orders/components/OrderDetail.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useParams, useNavigate } from "react-router"; 3 | import { 4 | useOrder, 5 | useUpdateOrder, 6 | useDeleteOrder, 7 | } from "../queries/OrderQueries"; 8 | import { Order, OrderItem, OrderStatus } from "../../../types"; 9 | import OrderDetailView from "./OrderDetailView"; 10 | import OrderForm from "./OrderForm"; 11 | 12 | const OrderDetail: React.FC = () => { 13 | const { id } = useParams<{ id: string }>(); 14 | const navigate = useNavigate(); 15 | 16 | // Query hooks 17 | const { data: order, isLoading, error } = useOrder(id!); 18 | const updateOrder = useUpdateOrder(); 19 | const deleteOrder = useDeleteOrder(); 20 | 21 | // Local state for edit mode 22 | const [isEditing, setIsEditing] = useState(false); 23 | // Local state for edit mode 24 | const [editForm, setEditForm] = useState< 25 | Omit 26 | >({ 27 | customerName: "", 28 | userId: "", // Add userId field 29 | status: OrderStatus.PENDING, 30 | shippingAddress: "", 31 | items: [], 32 | total: 0, 33 | orderDate: new Date().toISOString(), 34 | }); 35 | 36 | // Initialize form with order data when available 37 | useEffect(() => { 38 | if (order) { 39 | setEditForm({ 40 | customerName: order.customerName, 41 | userId: order.userId, // Add userId field 42 | status: order.status, 43 | shippingAddress: order.shippingAddress, 44 | items: [...order.items], 45 | total: order.total, 46 | orderDate: order.orderDate, 47 | }); 48 | } 49 | }, [order]); 50 | 51 | // Handle form changes 52 | const handleInputChange = ( 53 | e: React.ChangeEvent< 54 | HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement 55 | > 56 | ) => { 57 | const { name, value } = e.target; 58 | setEditForm((prev) => ({ 59 | ...prev, 60 | [name]: value, 61 | })); 62 | }; 63 | 64 | // Handle adding a new item 65 | const handleAddItem = () => { 66 | setEditForm((prev) => ({ 67 | ...prev, 68 | items: [ 69 | ...prev.items, 70 | { productId: "", name: "", quantity: 1, price: 0 }, 71 | ], 72 | })); 73 | }; 74 | 75 | // Handle removing an item 76 | const handleRemoveItem = (index: number) => { 77 | setEditForm((prev) => ({ 78 | ...prev, 79 | items: prev.items.filter((_, i) => i !== index), 80 | })); 81 | }; 82 | 83 | // Handle item field changes 84 | const handleItemChange = ( 85 | index: number, 86 | field: keyof OrderItem, 87 | value: any 88 | ) => { 89 | setEditForm((prev) => { 90 | const updatedItems = [...prev.items]; 91 | updatedItems[index] = { 92 | ...updatedItems[index], 93 | [field]: value, 94 | }; 95 | 96 | // Recalculate total 97 | const total = updatedItems.reduce( 98 | (sum, item) => sum + item.price * item.quantity, 99 | 0 100 | ); 101 | 102 | return { 103 | ...prev, 104 | items: updatedItems, 105 | total: total, 106 | }; 107 | }); 108 | }; 109 | 110 | // Handle form submission 111 | const handleSubmit = async (e: React.FormEvent) => { 112 | e.preventDefault(); 113 | if (!id) return; 114 | 115 | try { 116 | await updateOrder.mutateAsync({ 117 | id, 118 | data: editForm, 119 | }); 120 | setIsEditing(false); 121 | } catch (error) { 122 | console.error("Failed to update order:", error); 123 | } 124 | }; 125 | 126 | // Handle order deletion 127 | const handleDelete = async () => { 128 | if (!id) return; 129 | 130 | if (window.confirm("Are you sure you want to delete this order?")) { 131 | try { 132 | await deleteOrder.mutateAsync(id); 133 | navigate("/orders"); 134 | } catch (error) { 135 | console.error("Failed to delete order:", error); 136 | } 137 | } 138 | }; 139 | 140 | if (isLoading) { 141 | return ( 142 |
143 | Loading... 144 |
145 | ); 146 | } 147 | 148 | if (error) { 149 | return ( 150 |
Error: {error.message}
151 | ); 152 | } 153 | 154 | if (!order) { 155 | return
Order not found
; 156 | } 157 | 158 | if (isEditing) { 159 | return ( 160 | setIsEditing(false)} 168 | isSubmitting={updateOrder.isPending} 169 | title="Edit Order" 170 | submitLabel="Update Order" 171 | /> 172 | ); 173 | } 174 | 175 | return ( 176 | setIsEditing(true)} 179 | onDelete={handleDelete} 180 | isDeleting={deleteOrder.isPending} 181 | /> 182 | ); 183 | }; 184 | 185 | export default OrderDetail; 186 | -------------------------------------------------------------------------------- /src/pages/orders/components/OrderDetailView.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Order, OrderStatus } from "../../../types"; 3 | 4 | interface OrderDetailViewProps { 5 | order: Order; 6 | onEdit: () => void; 7 | onDelete: () => void; 8 | isDeleting: boolean; 9 | } 10 | 11 | const OrderDetailView: React.FC = ({ 12 | order, 13 | onEdit, 14 | onDelete, 15 | isDeleting, 16 | }) => { 17 | return ( 18 |
19 |
20 |
21 |
22 |
23 |

24 | Order #{order.id.slice(-6)} 25 |

26 |

27 | {new Date(order.orderDate).toLocaleString()} 28 |

29 |
30 |
31 | 37 | 44 |
45 |
46 |
47 | 48 |
49 |
50 |
51 |

Customer Information

52 |

53 | Name: {order.customerName} 54 |

55 |

56 | Status:{" "} 57 | 66 | {order.status.toUpperCase()} 67 | 68 |

69 |

70 | Total: $ 71 | {order.total.toFixed(2)} 72 |

73 |
74 | 75 |
76 |

Shipping Address

77 |

78 | {order.shippingAddress} 79 |

80 |
81 |
82 | 83 |
84 |

Order Items

85 |
86 | 87 | 88 | 89 | 95 | 101 | 107 | 113 | 114 | 115 | 116 | {order.items.map((item, index) => ( 117 | 118 | 126 | 129 | 132 | 135 | 136 | ))} 137 | 138 | 139 | 140 | 146 | 149 | 150 | 151 |
93 | Product 94 | 99 | Quantity 100 | 105 | Price 106 | 111 | Subtotal 112 |
119 |
120 | {item.name} 121 |
122 |
123 | ID: {item.productId} 124 |
125 |
127 | {item.quantity} 128 | 130 | ${item.price.toFixed(2)} 131 | 133 | ${(item.price * item.quantity).toFixed(2)} 134 |
144 | Total: 145 | 147 | ${order.total.toFixed(2)} 148 |
152 |
153 |
154 |
155 |
156 |
157 | ); 158 | }; 159 | 160 | export default OrderDetailView; 161 | -------------------------------------------------------------------------------- /src/pages/orders/components/OrderForm.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Order, OrderItem } from "../../../types"; 3 | 4 | interface OrderFormProps { 5 | order: Omit; 6 | onSubmit: (e: React.FormEvent) => Promise; 7 | onChange: ( 8 | e: React.ChangeEvent< 9 | HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement 10 | > 11 | ) => void; 12 | onAddItem: () => void; 13 | onRemoveItem: (index: number) => void; 14 | onItemChange: (index: number, field: keyof OrderItem, value: any) => void; 15 | onCancel: () => void; 16 | isSubmitting: boolean; 17 | title: string; 18 | submitLabel: string; 19 | } 20 | 21 | const OrderForm: React.FC = ({ 22 | order, 23 | onSubmit, 24 | onChange, 25 | onAddItem, 26 | onRemoveItem, 27 | onItemChange, 28 | onCancel, 29 | isSubmitting, 30 | title, 31 | submitLabel, 32 | }) => { 33 | return ( 34 |
35 |
36 |
37 |

{title}

38 | 56 |
57 |
58 |
59 | 62 | 70 |
71 | 72 |
73 | 76 | 88 |
89 | 90 |
91 | 94 |