diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index 540eb96..0e3ddb2 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -5,9 +5,27 @@ on: - main pull_request: jobs: + lint-and-typecheck: + name: Lint and Type Check + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + cache: 'npm' + - name: Install dependencies + run: npm ci + - name: Run type-check + run: npm run type-check + - name: Run lint + run: npm run lint build-and-push-docker-image: name: Build Docker image and publish runs-on: ubuntu-latest + needs: lint-and-typecheck steps: - name: Checkout code uses: actions/checkout@v3 diff --git a/TYPESCRIPT_MIGRATION.md b/TYPESCRIPT_MIGRATION.md new file mode 100644 index 0000000..c8a1550 --- /dev/null +++ b/TYPESCRIPT_MIGRATION.md @@ -0,0 +1,133 @@ +# TypeScript Migration Guide + +This project has been migrated to TypeScript with Redux Toolkit. This document provides guidance for developers working with the new Redux setup. + +## Redux Toolkit Store Structure + +The Redux store is now managed using Redux Toolkit with the following structure: + +``` +src/ + store/ + index.ts # Store configuration + hooks.ts # Typed hooks (useAppSelector, useAppDispatch) + slices/ + appSlice.ts # App state (search, filters, terms) + coursesSlice.ts # Course data and search + instructorsSlice.ts # Instructor data and search + subjectsSlice.ts # Subject data and search + gradesSlice.ts # Grade distributions + exploreSlice.ts # Explore page data +``` + +## Using TypeScript with Redux + +### Typed Hooks + +Use the typed hooks instead of plain `useDispatch` and `useSelector`: + +```typescript +import { useAppDispatch, useAppSelector } from './store/hooks'; + +function MyComponent() { + const dispatch = useAppDispatch(); + const courses = useAppSelector((state) => state.courses); + // ... +} +``` + +### Dispatching Actions + +All async actions use `createAsyncThunk`: + +```typescript +import { fetchCourse } from './store/slices/coursesSlice'; + +function MyComponent() { + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(fetchCourse('some-uuid')); + }, [dispatch]); +} +``` + +### Accessing State with Selectors + +Use the exported selectors from each slice: + +```typescript +import { selectCourse } from './store/slices/coursesSlice'; +import { useAppSelector } from './store/hooks'; + +function CourseDetails({ uuid }) { + const course = useAppSelector(selectCourse(uuid)); + // ... +} +``` + +## TypeScript Configuration + +### Strict Mode + +The project uses strict TypeScript settings: +- `strict: true` +- `noImplicitAny: true` +- `strictNullChecks: true` +- All strict checks enabled + +### Type Checking + +Run type checking: +```bash +npm run type-check +``` + +### Linting + +Run ESLint: +```bash +npm run lint +``` + +Note: The lint configuration currently allows `any` types as warnings during the migration. These should be gradually replaced with proper types. + +## CI/CD Integration + +The GitHub Actions workflow now includes: +1. **Type checking** - Ensures no TypeScript errors +2. **Linting** - Checks code quality and style +3. **Build** - Builds the project + +All checks must pass before merging PRs. + +## Migration Status + +### Completed +- ✅ All Redux slices migrated to TypeScript +- ✅ Redux Toolkit with `createAsyncThunk` for all async operations +- ✅ Typed `RootState` and `AppDispatch` +- ✅ Typed hooks (`useAppSelector`, `useAppDispatch`) +- ✅ CI/CD pipeline with type-check and lint +- ✅ Build system configured for TypeScript + +### Future Work +- Migrate React components to TypeScript +- Add proper types for API responses +- Replace `any` types with proper interfaces +- Add unit tests for slices +- Consider RTK Query for data fetching + +## Best Practices + +1. **Always use typed hooks** - Never use plain `useDispatch` or `useSelector` +2. **Use selectors** - Export and use selector functions from slices +3. **Type your components** - As you touch components, convert them to TypeScript +4. **Avoid `any`** - Use proper types or create interfaces for complex data +5. **Test your changes** - Run `npm run type-check` and `npm run build` before committing + +## Resources + +- [Redux Toolkit Documentation](https://redux-toolkit.js.org/) +- [TypeScript Documentation](https://www.typescriptlang.org/) +- [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app/) diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..0fece1a --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,65 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { ignores: ['build', 'node_modules'] }, + { + extends: [js.configs.recommended], + files: ['**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + plugins: { + 'react-hooks': reactHooks, + }, + rules: { + ...reactHooks.configs.recommended.rules, + }, + }, + { + extends: [js.configs.recommended, ...tseslint.configs.strictTypeChecked], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unsafe-assignment': 'warn', + '@typescript-eslint/no-unsafe-call': 'warn', + '@typescript-eslint/no-unsafe-member-access': 'warn', + '@typescript-eslint/no-unsafe-return': 'warn', + '@typescript-eslint/no-unsafe-argument': 'warn', + '@typescript-eslint/no-invalid-void-type': 'warn', + '@typescript-eslint/no-unnecessary-type-assertion': 'warn', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-unused-vars': ['error', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_' + }], + }, + }, +); diff --git a/package-lock.json b/package-lock.json index 25ef8ca..9614ab0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "madgrades", "version": "0.1.0", "dependencies": { + "@reduxjs/toolkit": "^2.11.2", "ajv": "^8.17.1", "axios": "^1.12.0", "classnames": "^2.2.5", @@ -35,8 +36,26 @@ "universal-cookie": "^7.2.1" }, "devDependencies": { + "@eslint/js": "^9.15.0", + "@types/dom-to-image": "^2.6.7", + "@types/file-saver": "^2.0.7", + "@types/lodash": "^4.17.23", + "@types/node": "^25.2.1", + "@types/qs": "^6.14.0", + "@types/react": "^19.2.13", + "@types/react-dom": "^19.2.3", + "@types/redux-logger": "^3.0.13", + "@typescript-eslint/eslint-plugin": "^8.54.0", + "@typescript-eslint/parser": "^8.54.0", "@vitejs/plugin-react": "^5.1.3", + "eslint": "^9.39.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.0", + "globals": "^17.3.0", "surge": "^0.24.6", + "typescript": "^5.9.3", + "typescript-eslint": "^8.54.0", "vite": "^7.3.1", "vite-plugin-html": "^3.2.2", "vite-plugin-sass-dts": "^1.3.35" @@ -200,9 +219,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { @@ -302,9 +321,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -807,6 +826,232 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@fluentui/react-component-event-listener": { "version": "0.63.1", "resolved": "https://registry.npmjs.org/@fluentui/react-component-event-listener/-/react-component-event-listener-0.63.1.tgz", @@ -820,6 +1065,58 @@ "react-dom": "^16.8.0 || ^17 || ^18" } }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -920,17 +1217,17 @@ } }, "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.3", "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">= 10.0.0" @@ -940,25 +1237,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", "cpu": [ "arm64" ], @@ -976,9 +1273,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", "cpu": [ "arm64" ], @@ -996,9 +1293,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", "cpu": [ "x64" ], @@ -1016,9 +1313,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", "cpu": [ "x64" ], @@ -1036,9 +1333,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", "cpu": [ "arm" ], @@ -1056,9 +1353,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", "cpu": [ "arm" ], @@ -1076,9 +1373,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", "cpu": [ "arm64" ], @@ -1096,9 +1393,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", "cpu": [ "arm64" ], @@ -1116,9 +1413,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", "cpu": [ "x64" ], @@ -1136,9 +1433,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", "cpu": [ "x64" ], @@ -1156,9 +1453,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], @@ -1176,9 +1473,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ "ia32" ], @@ -1196,9 +1493,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "cpu": [ "x64" ], @@ -1226,6 +1523,32 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@remix-run/router": { "version": "1.23.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", @@ -1256,6 +1579,19 @@ "node": ">= 8.0.0" } }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", @@ -1620,6 +1956,18 @@ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1714,9 +2062,9 @@ } }, "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -1743,6 +2091,13 @@ "@types/ms": "*" } }, + "node_modules/@types/dom-to-image": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/@types/dom-to-image/-/dom-to-image-2.6.7.tgz", + "integrity": "sha512-me5VbCv+fcXozblWwG13krNBvuEOm6kA5xoa4RrjDJCNFOZSWR3/QLtOXimBHk1Fisq69Gx3JtOoXtg1N1tijg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1758,6 +2113,13 @@ "@types/estree": "*" } }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -1767,6 +2129,20 @@ "@types/unist": "*" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -1782,30 +2158,302 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, - "node_modules/@types/react": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz", - "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", + "node_modules/@types/node": { + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.1.tgz", + "integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { - "csstype": "^3.2.2" + "undici-types": "~7.16.0" } }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, "license": "MIT" }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", + "node_modules/@types/react": { + "version": "19.2.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", + "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/redux-logger": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.13.tgz", + "integrity": "sha512-jylqZXQfMxahkuPcO8J12AKSSCQngdEWQrw7UiLUJzMBcv1r4Qg77P6mjGLjM27e5gFQDPD8vwUMJ9AyVxFSsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", + "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.54.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", + "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", + "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", + "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", + "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", + "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", + "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", + "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@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==", "license": "ISC" @@ -1831,22 +2479,13 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/@vitejs/plugin-react/node_modules/react-refresh": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1854,6 +2493,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -1881,17 +2530,28 @@ } }, "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/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", @@ -1908,6 +2568,105 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", @@ -2004,9 +2763,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -2041,9 +2800,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.30", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz", - "integrity": "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==", + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2081,20 +2840,20 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -2104,9 +2863,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", - "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -2125,11 +2884,11 @@ "license": "MIT", "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.8.25", - "caniuse-lite": "^1.0.30001754", - "electron-to-chromium": "^1.5.249", + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", - "update-browserslist-db": "^1.1.4" + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -2192,6 +2951,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camel-case": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", @@ -2214,9 +2983,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001756", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz", - "integrity": "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==", + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", "dev": true, "funding": [ { @@ -2252,17 +3021,20 @@ } }, "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/character-entities": { @@ -2305,6 +3077,21 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -2350,18 +3137,23 @@ } }, "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/colorette": { @@ -2467,19 +3259,18 @@ "license": "MIT" }, "node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=4.8" + "node": ">= 8" } }, "node_modules/css-select": { @@ -2549,9 +3340,9 @@ } }, "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", "license": "ISC", "engines": { "node": ">=12" @@ -2727,9 +3518,9 @@ "license": "MIT" }, "node_modules/decode-named-character-reference": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", - "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", "license": "MIT", "dependencies": { "character-entities": "^2.0.0" @@ -2743,6 +3534,14 @@ "version": "0.3.8", "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz", "integrity": "sha512-yVn6RZmHiGnxRKR9sJb3iVV2XTF1Ghh2DiWRZ3dMnGc43yUdWWF/kX6lQyk3+P84iprfWKU/8zFTrlkvtFm1ug==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, "license": "MIT" }, "node_modules/define-data-property": { @@ -2798,16 +3597,13 @@ } }, "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, "node_modules/devlop": { @@ -2823,6 +3619,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -2973,12 +3782,19 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.259", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz", - "integrity": "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==", + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", @@ -2999,9 +3815,9 @@ } }, "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", @@ -3084,6 +3900,34 @@ "node": ">= 0.4" } }, + "node_modules/es-iterator-helpers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.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", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -3111,6 +3955,19 @@ "node": ">= 0.4" } }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-to-primitive": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", @@ -3181,12 +4038,342 @@ } }, "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.8.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-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, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.0.tgz", + "integrity": "sha512-ZYvmh7VfVgqR/7wR71I3Zl6hK/C5CcxdWYKZSpHawS5JCNgE4efhQWg/+/WPpgGAp9Ngp/rRZYyaIwmPQBq/lA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=9" + } + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" } }, "node_modules/estree-util-is-identifier-name": { @@ -3206,6 +4393,16 @@ "dev": true, "license": "MIT" }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -3241,9 +4438,9 @@ "license": "MIT" }, "node_modules/fast-equals": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.3.tgz", - "integrity": "sha512-/boTcHZeIAQ2r/tL11voclBHDeP9WPxLt+tyAbVSyyXuUFyh0Tne7gJZTqGbxnvj79TjLdCXLOY7UIPhyG5MTw==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", "license": "MIT", "engines": { "node": ">=6.0.0" @@ -3266,6 +4463,19 @@ "node": ">=8.6.0" } }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3273,6 +4483,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -3286,17 +4503,48 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ], - "license": "BSD-3-Clause" + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "reusify": "^1.0.4" + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" } }, "node_modules/file-saver": { @@ -3315,16 +4563,6 @@ "minimatch": "^5.0.1" } }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/filelist/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -3342,7 +4580,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -3351,6 +4589,44 @@ "node": ">=8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -3427,6 +4703,16 @@ "node": ">=12" } }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3466,20 +4752,6 @@ "node": ">=0.6" } }, - "node_modules/fstream/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3605,7 +4877,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -3624,16 +4896,53 @@ } }, "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", + "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { @@ -3732,12 +5041,13 @@ } }, "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/has-property-descriptors": { @@ -3856,6 +5166,23 @@ "he": "bin/he" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -3917,12 +5244,59 @@ "dev": true, "license": "MIT" }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", + "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/immutable": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", "license": "MIT" }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -4250,7 +5624,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -4446,6 +5820,24 @@ "dev": true, "license": "MIT" }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/jake": { "version": "10.9.4", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", @@ -4465,9 +5857,9 @@ } }, "node_modules/jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-4.0.0.tgz", + "integrity": "sha512-TXCHVR3Lb6TZdtw1l3RTLf8RBWVGexdxL6AC8/e0xZKEpBflBsjh9/8LXw+dkNFuOyW9B7iB3O1sP7hS0Kiacg==", "license": "MIT" }, "node_modules/js-tokens": { @@ -4476,6 +5868,19 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -4496,6 +5901,13 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -4515,6 +5927,13 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -4548,6 +5967,16 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonfile/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/jsprim": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", @@ -4564,12 +5993,38 @@ "node": ">=0.6.0" } }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/keyboard-key": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keyboard-key/-/keyboard-key-1.1.0.tgz", "integrity": "sha512-qkBzPTi3rlAKvX7k0/ub44sqOfXeLc/jcnGGmj5c7BJpU8eDrEVPyhCvNYAaoubbsLm9uGWwQJO1ytQK1a9/dQ==", "license": "MIT" }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -4580,6 +6035,20 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -4595,26 +6064,20 @@ "node": ">=4" } }, - "node_modules/load-json-file/node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "license": "MIT", "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/load-json-file/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "license": "MIT", - "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash": { @@ -4629,6 +6092,13 @@ "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -5297,7 +6767,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -5307,6 +6777,19 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -5329,15 +6812,19 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -5401,6 +6888,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/netrc": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/netrc/-/netrc-0.1.4.tgz", @@ -5462,6 +6956,35 @@ "validate-npm-package-license": "^3.0.1" } }, + "node_modules/normalize-package-data/node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/normalize.css": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz", @@ -5493,6 +7016,166 @@ "node": ">= 4" } }, + "node_modules/npm-run-all/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm-run-all/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/npm-run-all/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/npm-run-all/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/npm-run-all/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/npm-run-all/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm-run-all/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-all/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -5566,6 +7249,60 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5573,24 +7310,74 @@ "dev": true, "license": "ISC", "dependencies": { - "wrappy": "1" + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/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==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/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==", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" + "p-limit": "^3.0.2" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/param-case": { @@ -5604,6 +7391,19 @@ "tslib": "^2.0.3" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", @@ -5629,6 +7429,19 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/pascal-case": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", @@ -5640,6 +7453,16 @@ "tslib": "^2.0.3" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -5651,12 +7474,13 @@ } }, "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/path-parse": { @@ -5665,6 +7489,18 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/pathe": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/pathe/-/pathe-0.2.0.tgz", @@ -5687,13 +7523,13 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "devOptional": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -5785,6 +7621,16 @@ "postcss": "^8.4.21" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", @@ -5920,9 +7766,9 @@ "license": "MIT" }, "node_modules/react": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", - "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", "peer": true, "engines": { @@ -5930,16 +7776,16 @@ } }, "node_modules/react-dom": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", - "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", "peer": true, "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.0" + "react": "^19.2.4" } }, "node_modules/react-fast-compare": { @@ -6001,6 +7847,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -6019,6 +7866,16 @@ } } }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-router": { "version": "6.30.3", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", @@ -6109,16 +7966,17 @@ "node": ">=4" } }, - "node_modules/read-pkg/node_modules/path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "license": "MIT", - "dependencies": { - "pify": "^3.0.0" - }, "engines": { - "node": ">=4" + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/recharts": { @@ -6328,26 +8186,40 @@ "dev": true, "license": "MIT" }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "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, "license": "MIT", "dependencies": { - "is-core-module": "^2.16.1", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, - "engines": { - "node": ">= 0.4" - }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -6359,6 +8231,65 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6841,16 +8772,6 @@ "node": ">=14.0.0" } }, - "node_modules/sass-embedded/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/sass-embedded/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -6867,34 +8788,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/sass/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/sass/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -6944,12 +8837,16 @@ } }, "node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, "license": "ISC", "bin": { - "semver": "bin/semver" + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/set-function-length": { @@ -7005,24 +8902,26 @@ "license": "MIT" }, "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { - "shebang-regex": "^1.0.0" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/shell-quote": { @@ -7252,16 +9151,37 @@ "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=8" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string-width/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, "node_modules/string.prototype.padend": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", @@ -7280,6 +9200,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", @@ -7363,6 +9294,28 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -7382,15 +9335,16 @@ } }, "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -7446,16 +9400,6 @@ "minimatch": "7.4.6" } }, - "node_modules/surge-fstream-ignore/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/surge-fstream-ignore/node_modules/minimatch": { "version": "7.4.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", @@ -7547,9 +9491,9 @@ } }, "node_modules/terser": { - "version": "5.44.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", - "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7602,43 +9546,11 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/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==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -7663,16 +9575,6 @@ "node": ">=6" } }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -7693,6 +9595,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -7720,6 +9635,19 @@ "dev": true, "license": "Unlicense" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -7794,6 +9722,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", + "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.54.0", + "@typescript-eslint/parser": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -7812,6 +9779,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -7871,9 +9845,9 @@ } }, "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -7910,19 +9884,19 @@ } }, "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 10.0.0" + "node": ">= 4.0.0" } }, "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -8199,83 +10173,6 @@ "vite": "^7" } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vite/node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", - "fsevents": "~2.3.2" - } - }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", @@ -8286,15 +10183,19 @@ } }, "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, "bin": { - "which": "bin/which" + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, "node_modules/which-boxed-primitive": { @@ -8362,9 +10263,9 @@ } }, "node_modules/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==", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -8382,6 +10283,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -8406,6 +10317,43 @@ "dev": true, "license": "ISC" }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index a66a7ad..9f83c78 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,9 @@ "name": "madgrades", "version": "0.1.0", "private": true, + "type": "module", "dependencies": { + "@reduxjs/toolkit": "^2.11.2", "ajv": "^8.17.1", "axios": "^1.12.0", "classnames": "^2.2.5", @@ -38,6 +40,8 @@ "build-js": "vite build", "build": "npm-run-all git-info build-css build-js", "preview": "vite preview", + "type-check": "tsc --noEmit", + "lint": "eslint 'src/**/*.{ts,tsx}' --report-unused-disable-directives --max-warnings 300", "deploy": "npm run build && mv build/index.html build/200.html && surge build", "deploy-staging": "npm run build && mv build/index.html build/200.html && surge build staging.madgrades.com", "deploy-prod": "npm run build && mv build/index.html build/200.html && surge build madgrades.com" @@ -53,8 +57,26 @@ "npm": ">=6.0.0" }, "devDependencies": { + "@eslint/js": "^9.15.0", + "@types/dom-to-image": "^2.6.7", + "@types/file-saver": "^2.0.7", + "@types/lodash": "^4.17.23", + "@types/node": "^25.2.1", + "@types/qs": "^6.14.0", + "@types/react": "^19.2.13", + "@types/react-dom": "^19.2.3", + "@types/redux-logger": "^3.0.13", + "@typescript-eslint/eslint-plugin": "^8.54.0", + "@typescript-eslint/parser": "^8.54.0", "@vitejs/plugin-react": "^5.1.3", + "eslint": "^9.39.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.0", + "globals": "^17.3.0", "surge": "^0.24.6", + "typescript": "^5.9.3", + "typescript-eslint": "^8.54.0", "vite": "^7.3.1", "vite-plugin-html": "^3.2.2", "vite-plugin-sass-dts": "^1.3.35" diff --git a/src/App.js b/src/App.tsx similarity index 50% rename from src/App.js rename to src/App.tsx index 8b3f6e4..d37875b 100644 --- a/src/App.js +++ b/src/App.tsx @@ -1,13 +1,17 @@ -import React, { Component, useEffect } from "react"; -import { BrowserRouter, useLocation } from "react-router-dom"; +import { useEffect } from "react"; +import { BrowserRouter, useLocation, Location } from "react-router-dom"; import SiteHeader from "./containers/SiteHeader"; import SiteFooter from "./containers/SiteFooter"; import Routes from "./Routes"; -const updateGa = (location) => { - if (!location) { - location = window.location; +declare global { + interface Window { + gtag?: (command: string, eventName: string, params: Record) => void; + ga?: (command: string, field: string, value?: string) => void; } +} + +const updateGa = (location: Location): void => { if (window.gtag) { window.gtag("event", "page_view", { page_path: location.pathname + location.search + location.hash, @@ -21,8 +25,7 @@ const updateGa = (location) => { } }; -// Component to track route changes -function AnalyticsTracker() { +function AnalyticsTracker(): null { const location = useLocation(); useEffect(() => { @@ -32,21 +35,19 @@ function AnalyticsTracker() { return null; } -class App extends Component { - render = () => { - return ( - - -
- -
- -
- +function App() { + return ( + + +
+ +
+
- - ); - }; + +
+
+ ); } export default App; diff --git a/src/Routes.js b/src/Routes.tsx similarity index 92% rename from src/Routes.js rename to src/Routes.tsx index 4f17ffb..c70e8bb 100644 --- a/src/Routes.js +++ b/src/Routes.tsx @@ -8,7 +8,7 @@ import Explore from "./pages/Explore"; import { Route, Routes } from "react-router-dom"; import React from "react"; -export default () => ( +const AppRoutes: React.FC = () => ( } /> } /> @@ -20,3 +20,5 @@ export default () => ( } /> ); + +export default AppRoutes; diff --git a/src/components/CourseChart.js b/src/components/CourseChart.js deleted file mode 100644 index 553b4b5..0000000 --- a/src/components/CourseChart.js +++ /dev/null @@ -1,130 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; -import PropTypes from "prop-types"; -import utils from "../utils"; -import GradeDistributionChart from "../containers/charts/GradeDistributionChart"; -import { Dimmer, Loader } from "semantic-ui-react"; -import Div from "../containers/Div"; - -class CourseChart extends Component { - static propTypes = { - uuid: PropTypes.string.isRequired, - termCode: PropTypes.number, - instructorId: PropTypes.number, - }; - - componentDidMount = () => { - const { actions, uuid } = this.props; - - actions.fetchCourseGrades(uuid); - }; - - render = () => { - const { course, uuid, data, termCode, instructorId } = this.props; - - let chart, primary, label, secondary, secondaryLabel, isLoaded; - - let title = course && course.name; - title += ": Cumulative"; - - if (data && data.cumulative) { - isLoaded = true; - - primary = data.cumulative; - label = `Cumulative - ${utils.grades.gpa(data.cumulative, true)} GPA`; - - let termName = termCode && utils.termCodes.toName(termCode); - - if (termCode && !instructorId) { - let offering = data.courseOfferings.filter( - (o) => o.termCode === termCode - )[0]; - - if (offering) { - secondary = offering.cumulative; - secondaryLabel = `${termName}`; - title += ` vs. ${termName}`; - } else { - console.error(`Invalid course/term combination: ${uuid}/${termCode}`); - } - } else if (instructorId && !termCode) { - let instructor = data.instructors.filter( - (i) => i.id === instructorId - )[0]; - - if (instructor) { - secondary = instructor.cumulative; - secondaryLabel = instructor.name; - title += ` vs. ${instructor.name}`; - } else { - console.error( - `Invalid course/instructor combination: ${uuid}/${instructorId}` - ); - } - } else if (instructorId && termCode) { - let instructor = data.instructors.filter( - (i) => i.id === instructorId - )[0]; - - if (instructor) { - let offering = instructor.terms.filter( - (o) => o.termCode === termCode - )[0]; - - if (offering) { - secondary = offering; - secondaryLabel = `${instructor.name} (${termName})`; - title += ` vs. ${instructor.name} (${termName})`; - } else { - console.error( - `Invalid course/instructor/term combination: ${uuid}/${instructorId}/${termCode}` - ); - } - } - } else { - // no secondary - } - - if (secondary) { - secondaryLabel += " - " + utils.grades.gpa(secondary, true) + " GPA"; - } - } - - if (isLoaded) { - chart = ( - - ); - } else { - chart = ; - } - - return ( - - - - Loading Data - - - {chart} - - ); - }; -} - -function mapStateToProps(state, ownProps) { - const course = state.courses.data[ownProps.uuid]; - const data = state.grades.courses.data[ownProps.uuid]; - - return { - course, - data, - }; -} - -export default connect(mapStateToProps, utils.mapDispatchToProps)(CourseChart); diff --git a/src/components/CourseChart.tsx b/src/components/CourseChart.tsx new file mode 100644 index 0000000..d36ac8c --- /dev/null +++ b/src/components/CourseChart.tsx @@ -0,0 +1,124 @@ +import React, { useEffect } from "react"; +import { useAppDispatch, useAppSelector } from "../store/hooks"; +import { fetchCourseGrades } from "../store/slices/gradesSlice"; +import utils from "../utils"; +import GradeDistributionChart from "../containers/charts/GradeDistributionChart"; +import { Dimmer, Loader } from "semantic-ui-react"; +import Div from "../containers/Div"; +import { GradeDistribution } from "../types/api"; + +interface CourseChartProps { + uuid: string; + termCode?: number; + instructorId?: number; +} + +const CourseChart: React.FC = ({ uuid, termCode, instructorId }) => { + const dispatch = useAppDispatch(); + const course = useAppSelector(state => state.courses.data[uuid]); + const data = useAppSelector(state => state.grades.courses.data[uuid]); + + useEffect(() => { + dispatch(fetchCourseGrades(uuid)); + }, [dispatch, uuid]); + + let chart: React.ReactNode; + let primary: GradeDistribution | undefined; + let label: string | undefined; + let secondary: GradeDistribution | undefined; + let secondaryLabel: string | undefined; + let isLoaded = false; + + let title = course?.data?.name || ''; + title += ": Cumulative"; + + const cumulative = data?.courseOfferings ? utils.grades.combineAll(data.courseOfferings.map(o => o.cumulative)) : undefined; + + if (data && cumulative) { + isLoaded = true; + + primary = cumulative; + label = `Cumulative - ${utils.grades.gpa(cumulative, true)} GPA`; + + const termName = termCode && utils.termCodes.toName(termCode); + + if (termCode && !instructorId) { + const offering = data.courseOfferings?.filter( + (o) => o.termCode === termCode + )[0]; + + if (offering) { + secondary = offering.cumulative; + secondaryLabel = `${termName}`; + title += ` vs. ${termName}`; + } else { + console.error(`Invalid course/term combination: ${uuid}/${termCode}`); + } + } else if (instructorId && !termCode) { + const instructor = data.instructors?.filter( + (i) => i.id === instructorId + )[0]; + + if (instructor) { + secondary = instructor.cumulative; + secondaryLabel = instructor.name; + title += ` vs. ${instructor.name}`; + } else { + console.error( + `Invalid course/instructor combination: ${uuid}/${instructorId}` + ); + } + } else if (instructorId && termCode) { + const instructor = data.instructors?.filter( + (i) => i.id === instructorId + )[0]; + + if (instructor) { + const offering = instructor.terms.filter( + (o) => o.termCode === termCode + )[0]; + + if (offering) { + secondary = offering; + secondaryLabel = `${instructor.name} (${termName})`; + title += ` vs. ${instructor.name} (${termName})`; + } else { + console.error( + `Invalid course/instructor/term combination: ${uuid}/${instructorId}/${termCode}` + ); + } + } + } + + if (secondary) { + secondaryLabel += " - " + utils.grades.gpa(secondary, true) + " GPA"; + } + } + + if (isLoaded) { + chart = ( + + ); + } else { + chart = ; + } + + return ( + + + + Loading Data + + + {chart} + + ); +}; + +export default CourseChart; diff --git a/src/components/CourseChartViewer.js b/src/components/CourseChartViewer.js deleted file mode 100644 index 13c2f48..0000000 --- a/src/components/CourseChartViewer.js +++ /dev/null @@ -1,231 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; -import utils from "../utils"; -import PropTypes from "prop-types"; -import { Button, Dropdown, Form } from "semantic-ui-react"; -import { Row, Col } from "./Grid"; -import TermSelect from "../containers/TermSelect"; -import CourseChart from "./CourseChart"; -import domtoimage from "dom-to-image"; -import FileSaver from "file-saver"; - -class CourseChartViewer extends Component { - static propTypes = { - uuid: PropTypes.string.isRequired, - termCode: PropTypes.number, - instructorId: PropTypes.number, - onChange: PropTypes.func, - }; - - state = { - isExporting: false, - }; - - static defaultProps = { - onChange: ({ termCode, instructorId }) => {}, - }; - - fetchCourseGrades = () => { - const { uuid, actions } = this.props; - actions.fetchCourseGrades(uuid); - }; - - componentDidMount = this.fetchCourseGrades; - - componentDidUpdate = (prevProps) => { - if (prevProps.uuid !== this.props.uuid) { - this.fetchCourseGrades(); - } - }; - - onTermCodeChange = (termCode) => { - const { onChange, instructorId } = this.props; - - this.setState( - { - termCode, - }, - () => { - onChange({ termCode, instructorId }); - } - ); - }; - - onInstructorChange = (event, { value }) => { - const { onChange, termCode } = this.props; - - this.setState( - { - instructorId: value, - }, - () => { - onChange({ termCode, instructorId: value }); - } - ); - }; - - onSaveChart = () => { - if (this.state.isExporting) return; - - this.setState({ - isExporting: true, - }); - - domtoimage - .toBlob(this.chart, { bgcolor: "#fff" }) - .then((blob) => { - FileSaver.saveAs(blob, `madgrades-${new Date().toISOString()}.png`); - this.setState({ - isExporting: false, - }); - }) - .catch((error) => { - this.setState({ - isExporting: false, - }); - }); - }; - - render = () => { - const { uuid, data, instructorId, termCode } = this.props; - const { isExporting } = this.state; - - let instructorOptions = [], - termCodes = [], - termDescs = {}, - instructorText = "All instructors", - termText = "All semesters"; - - if (data && !data.isFetching) { - instructorOptions.push({ - key: 0, - value: 0, - text: instructorText, - }); - instructorOptions = instructorOptions.concat( - data.instructors.map((i) => { - return { - key: i.id, - value: i.id, - text: i.name, - description: utils.grades.gpa(i.cumulative, true), - }; - }) - ); - - data.courseOfferings.forEach((o) => { - termCodes.push(o.termCode); - termDescs[o.termCode] = utils.grades.gpa(o.cumulative, true); - }); - - // if instructor selected, filter term codes - if (instructorId) { - let instructorName = "N/A"; - - termCodes = termCodes.filter((code) => { - if (code === 0) return true; - - const instructor = data.instructors.filter( - (i) => i.id === instructorId - )[0]; - - if (!instructor) return true; - - instructorName = instructor.name; - return instructor.terms.map((term) => term.termCode).includes(code); - }); - - termText += ` (${instructorName})`; - } - - // if term code selected, filter instructor options - if (termCode) { - let termName = utils.termCodes.toName(termCode); - instructorText += ` (${termName})`; - - instructorOptions = instructorOptions.filter((option) => { - const id = option.value; - - if (id === 0) return true; - - const instructor = data.instructors.filter((i) => i.id === id)[0]; - return instructor.terms - .map((term) => term.termCode) - .includes(termCode); - }); - } - - instructorOptions[0].text = instructorText; - } - - let instructorChosen = instructorId || undefined, - termChosen = termCode || undefined; - - return ( - - -
- - - - - - - - - - - -
- - - - -
- - - - - - -
- + + + + + +
+ + + + + + +
+ - - ); - }; -} - -function mapStateToProps(state, ownProps) { - return { - courseFilterParams: state.app.courseFilterParams, - }; -} - -// HOC to inject navigate as prop -function withNavigate(Component) { - return function ComponentWithNavigate(props) { - const navigate = useNavigate(); - return ; - }; -} - -export default connect( - mapStateToProps, - utils.mapDispatchToProps -)(withNavigate(CourseFilterForm)); diff --git a/src/components/CourseFilterForm.tsx b/src/components/CourseFilterForm.tsx new file mode 100644 index 0000000..a54d160 --- /dev/null +++ b/src/components/CourseFilterForm.tsx @@ -0,0 +1,105 @@ +import React, { useEffect, useState } from "react"; +import { useAppSelector } from "../store/hooks"; +import { Button, Divider, Form, Input, InputOnChangeData } from "semantic-ui-react"; +import EntitySelect from "./EntitySelect"; +import { useNavigate } from "react-router-dom"; +import { stringify } from "qs"; +import * as _ from "lodash"; +import { CourseFilterParams } from "../types/api"; + +const CourseFilterForm: React.FC = () => { + const navigate = useNavigate(); + const courseFilterParams = useAppSelector(state => state.app.courseFilterParams); + const [subjects, setSubjects] = useState([]); + const [instructors, setInstructors] = useState([]); + const [query, setQuery] = useState(""); + + useEffect(() => { + if (courseFilterParams) { + const normalizedSubjects = Array.isArray(courseFilterParams.subjects) + ? courseFilterParams.subjects + : (courseFilterParams.subjects ? [courseFilterParams.subjects] : []); + const normalizedInstructors = Array.isArray(courseFilterParams.instructors) + ? courseFilterParams.instructors.map(i => typeof i === 'number' ? i : parseInt(String(i), 10)) + : (courseFilterParams.instructors ? [typeof courseFilterParams.instructors === 'number' ? courseFilterParams.instructors : parseInt(String(courseFilterParams.instructors), 10)] : []); + + setSubjects(normalizedSubjects); + setInstructors(normalizedInstructors); + setQuery(courseFilterParams.query || ""); + } + }, [courseFilterParams]); + + const onSubjectChange = (newSubjects: string[]): void => { + setSubjects(newSubjects); + }; + + const onInstructorChange = (newInstructors: number[]): void => { + setInstructors(newInstructors); + }; + + const onQueryChange = (_event: React.ChangeEvent, { value }: InputOnChangeData): void => { + setQuery(value); + }; + + const onClear = (event: React.MouseEvent): void => { + event.preventDefault(); + setSubjects([]); + setInstructors([]); + setQuery(""); + }; + + const onSubmit = (): void => { + const allParams: CourseFilterParams = { + ...(courseFilterParams || {}), + subjects, + instructors, + query, + page: 1, + }; + + if (courseFilterParams?.compareWith) { + allParams.compareWith = courseFilterParams.compareWith; + } + + const params = _.omitBy(allParams, _.isNil); + navigate("/search?" + stringify(params)); + }; + + return ( +
+ + + + + + + + onSubjectChange(value as string[])} + entityType="subject" + /> + + + + onInstructorChange(value as number[])} + entityType="instructor" + /> + + + Search + + + + ); +}; + +export default CourseFilterForm; diff --git a/src/components/CourseGpaChart.js b/src/components/CourseGpaChart.js deleted file mode 100644 index d6f6c70..0000000 --- a/src/components/CourseGpaChart.js +++ /dev/null @@ -1,53 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; -import utils from "../utils"; -import PropTypes from "prop-types"; -import { GpaChart } from "../containers/charts/GpaChart"; - -class CourseGpaChart extends Component { - static propTypes = { - uuid: PropTypes.string.isRequired, - }; - - fetchCourseGrades = () => { - this.props.actions.fetchCourseGrades(this.props.uuid); - }; - - componentDidMount = this.fetchCourseGrades; - - componentDidUpdate = (prevProps) => { - if (prevProps.uuid !== this.props.uuid) { - this.fetchCourseGrades(); - } - }; - - render = () => { - const { data } = this.props; - - if (!data || data.isFetching) return ; - - const gradeDistributions = data.courseOfferings - .map((o) => { - return { - ...o.cumulative, - termCode: o.termCode, - }; - }) - .sort((a, b) => a.termCode - b.termCode); - - return ; - }; -} - -function mapStateToProps(state, ownProps) { - const data = state.grades.courses.data[ownProps.uuid]; - - return { - data, - }; -} - -export default connect( - mapStateToProps, - utils.mapDispatchToProps -)(CourseGpaChart); diff --git a/src/components/CourseGpaChart.tsx b/src/components/CourseGpaChart.tsx new file mode 100644 index 0000000..6362867 --- /dev/null +++ b/src/components/CourseGpaChart.tsx @@ -0,0 +1,32 @@ +import React, { useEffect } from "react"; +import { useAppDispatch, useAppSelector } from "../store/hooks"; +import { fetchCourseGrades } from "../store/slices/gradesSlice"; +import { GpaChart } from "../containers/charts/GpaChart"; + +interface CourseGpaChartProps { + uuid: string; +} + +const CourseGpaChart: React.FC = ({ uuid }) => { + const dispatch = useAppDispatch(); + const data = useAppSelector(state => state.grades.courses.data[uuid]); + + useEffect(() => { + dispatch(fetchCourseGrades(uuid)); + }, [dispatch, uuid]); + + if (!data || data.isFetching) return ; + + const gradeDistributions = data.courseOfferings! + .map((o) => { + return { + ...o.cumulative, + termCode: o.termCode, + }; + }) + .sort((a, b) => a.termCode - b.termCode); + + return ; +}; + +export default CourseGpaChart; diff --git a/src/components/CourseName.js b/src/components/CourseName.js deleted file mode 100644 index f1b4228..0000000 --- a/src/components/CourseName.js +++ /dev/null @@ -1,71 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; -import PropTypes from "prop-types"; -import utils from "../utils"; -import SubjectNameList from "../containers/SubjectNameList"; - -class CourseName extends Component { - static propTypes = { - uuid: PropTypes.string.isRequired, - fallback: PropTypes.string, - data: PropTypes.object, - asSubjectAndNumber: PropTypes.bool, - }; - - fetchCourseIfNeeded = () => { - const { actions, uuid, data } = this.props; - - if (!data) { - actions.fetchCourse(uuid); - } - }; - - componentDidMount = this.fetchCourseIfNeeded; - - componentDidUpdate = (prevProps) => { - if (prevProps.uuid !== this.props.uuid) { - this.fetchCourseIfNeeded(); - } - }; - - render = () => { - const { name, subjects, number, fallback, asSubjectAndNumber } = this.props; - - if (asSubjectAndNumber) { - if (subjects) { - return ( - - {number} - - ); - } else { - return ( - - {fallback} {number} - - ); - } - } else { - return {name || fallback}; - } - }; -} - -function mapStateToProps(state, ownProps) { - const { uuid, data } = ownProps; - - let courseData = data; - - if (!data) { - const { courses } = state; - courseData = courses.data[uuid]; - } - - return { - name: courseData && courseData.name, - subjects: courseData && courseData.subjects, - number: courseData && courseData.number, - }; -} - -export default connect(mapStateToProps, utils.mapDispatchToProps)(CourseName); diff --git a/src/components/CourseName.tsx b/src/components/CourseName.tsx new file mode 100644 index 0000000..c68cdb9 --- /dev/null +++ b/src/components/CourseName.tsx @@ -0,0 +1,52 @@ +import React, { useEffect } from "react"; +import { useAppDispatch, useAppSelector } from "../store/hooks"; +import { fetchCourse } from "../store/slices/coursesSlice"; +import SubjectNameList from "../containers/SubjectNameList"; +import { Course } from "../types/api"; + +interface CourseNameProps { + uuid: string; + fallback?: string; + data?: Course; + asSubjectAndNumber?: boolean; +} + +const CourseName: React.FC = ({ uuid, data, fallback, asSubjectAndNumber }) => { + const dispatch = useAppDispatch(); + const courseStateData = useAppSelector(state => + !data ? state.courses.data[uuid] : undefined + ); + + // Get the actual course data from nested structure + const courseData = data || courseStateData?.data; + + useEffect(() => { + if (!data && !courseStateData) { + dispatch(fetchCourse(uuid)); + } + }, [dispatch, uuid, data, courseStateData]); + + const name = courseData?.name; + const subjects = courseData?.subjects; + const number = courseData?.number; + + if (asSubjectAndNumber) { + if (subjects) { + return ( + + {number} + + ); + } else { + return ( + + {fallback} {number} + + ); + } + } else { + return {name || fallback}; + } +}; + +export default CourseName; diff --git a/src/components/CourseSearchResults.js b/src/components/CourseSearchResults.js deleted file mode 100644 index 6faa54d..0000000 --- a/src/components/CourseSearchResults.js +++ /dev/null @@ -1,134 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; -import utils from "../utils"; -import { Dimmer, Icon, Loader, Pagination } from "semantic-ui-react"; -import { Row, Col } from "./Grid"; -import CourseSearchResultItem from "../containers/CourseSearchResultItem"; -import Div from "../containers/Div"; -import PropTypes from "prop-types"; -import * as _ from "lodash"; -import { useNavigate } from "react-router-dom"; -import { stringify } from "qs"; - -class CourseSearchResults extends Component { - static propTypes = { - courseFilterParams: PropTypes.object, - }; - - componentDidUpdate = (prevProps) => { - const { actions, courseFilterParams } = this.props; - - if (!_.isEqual(courseFilterParams, prevProps.courseFilterParams)) { - actions.fetchCourseSearch(courseFilterParams, courseFilterParams.page); - } - }; - - onPageChange = (event, data) => { - const { activePage } = data; - const { courseFilterParams, navigate } = this.props; - const params = { - ...courseFilterParams, - page: activePage, - }; - - // Preserve compareWith parameter if it exists - if (courseFilterParams.compareWith) { - params.compareWith = courseFilterParams.compareWith; - } - - navigate("/search?" + stringify(params)); - }; - - renderResults = (results) => - results.map((result) => { - return ( -
- -
- ); - }); - - render = () => { - const { isFetching } = this.props; - const { results, totalPages } = this.props.searchData; - - if (isFetching || (results && results.length > 0)) { - const { page } = this.props.courseFilterParams; - - return ( - - - - Loading - - - {this.renderResults(results || [])} - {results && results.length > 0 && ( - - - , - icon: true, - }} - firstItem={null} - lastItem={null} - prevItem={{ - content: , - icon: true, - }} - nextItem={{ - content: , - icon: true, - }} - totalPages={totalPages} - size="mini" - siblingRange={1} - /> - - - )} - - ); - } - - return ( -
-

No courses were found for your search.

-
- ); - }; -} - -function mapStateToProps(state) { - const { searchQuery, courseFilterParams } = state.app; - const { page } = courseFilterParams; - - let searchData, isFetching; - - const search = state.courses.search; - searchData = search.pages && search.pages[page]; - isFetching = search.isFetching; - - return { - searchQuery, - courseFilterParams, - isFetching, - searchData: searchData || {}, - }; -} - -// HOC to inject navigate as prop -function withNavigate(Component) { - return function ComponentWithNavigate(props) { - const navigate = useNavigate(); - return ; - }; -} - -export default connect( - mapStateToProps, - utils.mapDispatchToProps -)(withNavigate(CourseSearchResults)); diff --git a/src/components/CourseSearchResults.tsx b/src/components/CourseSearchResults.tsx new file mode 100644 index 0000000..0534bb6 --- /dev/null +++ b/src/components/CourseSearchResults.tsx @@ -0,0 +1,108 @@ +import React, { useEffect, useRef } from "react"; +import { useAppDispatch, useAppSelector } from "../store/hooks"; +import { fetchCourseSearch } from "../store/slices/coursesSlice"; +import { Dimmer, Icon, Loader, Pagination, PaginationProps } from "semantic-ui-react"; +import { Row, Col } from "./Grid"; +import CourseSearchResultItem from "../containers/CourseSearchResultItem"; +import Div from "../containers/Div"; +import { useNavigate } from "react-router-dom"; +import { stringify } from "qs"; +import { Course } from "../types/api"; + +const CourseSearchResults: React.FC = () => { + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const courseFilterParams = useAppSelector(state => state.app.courseFilterParams); + const search = useAppSelector(state => state.courses.search); + + const page = courseFilterParams.page || 1; + const searchData = search.pages?.[page]; + const isFetching = search.isFetching; + + // Use ref to track previous params string to avoid unnecessary fetches + const prevParamsStringRef = useRef(""); + + useEffect(() => { + const paramsString = JSON.stringify({ params: courseFilterParams, page }); + + // Only fetch if params actually changed + if (paramsString !== prevParamsStringRef.current) { + dispatch(fetchCourseSearch({ params: courseFilterParams, page })); + prevParamsStringRef.current = paramsString; + } + }, [dispatch, courseFilterParams, page]); + + const onPageChange = (_event: React.MouseEvent, data: PaginationProps): void => { + const { activePage } = data; + const params = { + ...courseFilterParams, + page: activePage as number, + }; + + if (courseFilterParams.compareWith) { + params.compareWith = courseFilterParams.compareWith; + } + + navigate("/search?" + stringify(params)); + }; + + const renderResults = (results: Course[]) => + results.map((result) => { + return ( +
+ +
+ ); + }); + + const results = searchData?.results; + const totalPages = searchData ? Math.ceil(searchData.total / searchData.perPage) : 1; + + if (isFetching || (results && results.length > 0)) { + return ( + + + + Loading + + + {renderResults(results || [])} + {results && results.length > 0 && ( + + + , + icon: true, + }} + firstItem={null} + lastItem={null} + prevItem={{ + content: , + icon: true, + }} + nextItem={{ + content: , + icon: true, + }} + totalPages={totalPages} + size="mini" + siblingRange={1} + /> + + + )} + + ); + } + + return ( +
+

No courses were found for your search.

+
+ ); +}; + +export default CourseSearchResults; diff --git a/src/components/CourseSortForm.js b/src/components/CourseSortForm.js deleted file mode 100644 index 7cb9d17..0000000 --- a/src/components/CourseSortForm.js +++ /dev/null @@ -1,110 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; -import utils from "../utils"; -import { Dropdown } from "semantic-ui-react"; -import { useNavigate } from "react-router-dom"; -import { stringify } from "qs"; - -const sortOptions = [ - { - key: "relevance", - text: "Best Match", - value: "relevance", - }, - { - key: "number", - text: "Number (Lowest First)", - value: "number", - }, - { - key: "number_desc", - text: "Number (Highest First)", - value: "number_desc", - }, -]; - -class CourseFilterForm extends Component { - state = { - value: "number", - }; - - componentDidUpdate = (prevProps) => { - if (prevProps.courseFilterParams !== this.props.courseFilterParams) { - const { sort, order } = this.props.courseFilterParams; - let value; - - if (!sort) { - value = "relevance"; - } else if (sort === "relevance") { - value = "relevance"; - } else if (sort === "number") { - value = "number"; - if (order === "desc") value = "number_desc"; - } - - if (value !== this.state.value) { - this.setState({ - value, - }); - } - } - }; - - onChange = (event, { value }) => { - this.setState({ - value, - }); - - let sort, order; - - if (value === "number") { - sort = "number"; - } else if (value === "number_desc") { - sort = "number"; - order = "desc"; - } else if (value === "relevance") { - // nothing to do - } - - const params = { - ...this.props.courseFilterParams, - sort, - order, - }; - this.props.navigate("/search?" + stringify(params, { encode: false })); - }; - - render = () => { - const { value } = this.state; - - return ( - - ); - }; -} - -function mapStateToProps(state, ownProps) { - return { - courseFilterParams: state.app.courseFilterParams, - }; -} - -// HOC to inject navigate as prop -function withNavigate(Component) { - return function ComponentWithNavigate(props) { - const navigate = useNavigate(); - return ; - }; -} - -export default connect( - mapStateToProps, - utils.mapDispatchToProps -)(withNavigate(CourseFilterForm)); diff --git a/src/components/CourseSortForm.tsx b/src/components/CourseSortForm.tsx new file mode 100644 index 0000000..b0d3ce4 --- /dev/null +++ b/src/components/CourseSortForm.tsx @@ -0,0 +1,92 @@ +import React, { useEffect, useState } from "react"; +import { useAppSelector } from "../store/hooks"; +import { Dropdown, DropdownProps } from "semantic-ui-react"; +import { useNavigate } from "react-router-dom"; +import { stringify } from "qs"; + +interface SortOption { + key: string; + text: string; + value: string; +} + +const sortOptions: SortOption[] = [ + { + key: "relevance", + text: "Best Match", + value: "relevance", + }, + { + key: "number", + text: "Number (Lowest First)", + value: "number", + }, + { + key: "number_desc", + text: "Number (Highest First)", + value: "number_desc", + }, +]; + +const CourseSortForm: React.FC = () => { + const navigate = useNavigate(); + const courseFilterParams = useAppSelector(state => state.app.courseFilterParams); + const [value, setValue] = useState("number"); + + useEffect(() => { + if (courseFilterParams) { + const { sort, order } = courseFilterParams; + let newValue: string; + + if (!sort) { + newValue = "relevance"; + } else if (sort === "relevance") { + newValue = "relevance"; + } else if (sort === "number") { + newValue = "number"; + if (order === "desc") newValue = "number_desc"; + } else { + newValue = "relevance"; + } + + if (newValue !== value) { + setValue(newValue); + } + } + }, [courseFilterParams, value]); + + const onChange = (_event: React.SyntheticEvent, { value: newValue }: DropdownProps): void => { + setValue(newValue as string); + + let sort: string | undefined, order: string | undefined; + + if (newValue === "number") { + sort = "number"; + } else if (newValue === "number_desc") { + sort = "number"; + order = "desc"; + } else if (newValue === "relevance") { + // nothing to do + } + + const params = { + ...(courseFilterParams || {}), + sort, + order, + }; + navigate("/search?" + stringify(params, { encode: false })); + }; + + return ( + + ); +}; + +export default CourseSortForm; diff --git a/src/components/EntitySelect.js b/src/components/EntitySelect.js deleted file mode 100644 index 59ec4c3..0000000 --- a/src/components/EntitySelect.js +++ /dev/null @@ -1,235 +0,0 @@ -import React, {Component} from 'react'; -import {connect} from 'react-redux'; -import utils from '../utils'; -import PropTypes from 'prop-types' -import {Dropdown} from 'semantic-ui-react'; -import _ from 'lodash'; - -/** - * A dropdown/search box for selecting a particular entity from madgrades. - * - * This is pretty fragile, easy to create infinite loops... :( - */ -class EntitySelect extends Component { - static propTypes = { - entityType: PropTypes.oneOf(['instructor', 'subject']).isRequired, - onChange: PropTypes.func, - value: PropTypes.array - }; - - static defaultProps = { - value: [], - onChange: (entityKey) => { } - }; - - state = { - query: '', - options: [], - isTyping: false, - isFetching: false - }; - - performSearch = (query) => { - switch (this.props.entityType) { - case 'instructor': - this.props.actions.fetchInstructorSearch(query, 1); - return; - case 'subject': - this.props.actions.fetchSubjectSearch(query, 1); - return; - default: - return; - } - }; - - requestEntity = (key, entityType) => { - switch (entityType) { - case 'instructor': - this.props.actions.fetchInstructor(key); - return; - case 'subject': - this.props.actions.fetchSubject(key); - return; - default: - return; - } - }; - - entityToKey = (entity, entityType) => { - switch (entityType) { - case 'instructor': - return entity.id; - case 'subject': - return entity.code; - default: - return; - } - }; - - entityToOption = (key, entity, entityType) => { - if (entity.isFetching) { - return { - key: key, - value: key, - text: `${key} (Loading...)` - } - } - else { - switch (entityType) { - case 'instructor': - return { - key: entity.id, - value: entity.id, - text: entity.name - }; - case 'subject': - return { - key: entity.code, - value: entity.code, - text: entity.name - }; - default: - return; - } - } - }; - - onChange = (event, { value }) => { - this.setState({ - query: '' - }); - this.props.onChange(value); - }; - - onSearchChange = (event, { searchQuery }) => { - this.setState({ - query: searchQuery, - isTyping: true - }); - - setTimeout(() => { - if (this.state.query === searchQuery) { - this.setState({ - isTyping: false - }); - this.performSearch(searchQuery); - } - }, 500); - }; - - componentDidUpdate = () => { - const { entityType, entityData, searches, value } = this.props; - const { query } = this.state; - - let searchData; - - if (query.length >= 2) { - searchData = searches[query] && searches[query][1]; - } - - let options = []; - let keys = new Set(); - - for (let keyStr of Object.keys(entityData)) { - let entity = entityData[keyStr]; - let key = this.entityToKey(entity, entityType); - options.push(this.entityToOption(key, entity, entityType)); - keys.add(key); - } - - for (let key of value) { - if (!keys.has(key)) { - options.push({ - key: key, - value: key, - text: `${key} (Loading...)` - }); - this.requestEntity(key, entityType); - keys.add(key); - } - } - - let isFetching = searchData && searchData.isFetching; - - // if we are searching, only show options found in the search - if (searchData && !searchData.isFetching) { - let keys = searchData.results.map(e => this.entityToKey(e, entityType)); - options = options.filter(o => keys.includes(o.key) || value.includes(o.key)); - } - - // otherwise the only options are the already selected values - else { - options = options.filter(o => value.includes(o.key)); - } - - // only update if options are new, we don't want infinite loop - if (!_.isEqual(this.state.options, options)) { - this.setState({ - options, - isFetching - }); - } - - if (this.state.isFetching !== isFetching) { - this.setState({ - isFetching - }); - } - }; - - componentDidMount = this.componentDidUpdate; - - render = () => { - const { options, isFetching, isTyping, query } = this.state; - const { value, entityType } = this.props; - - let message = 'No results found'; - if (query.length < 2) - message = 'Start typing to see results'; - else if (isTyping || isFetching) - message = 'Searching...'; - - return ( - options}/> - ) - } -} - -function mapStateToProps(state, ownProps) { - const { entityType } = ownProps; - - let entityState; - - switch (entityType) { - case 'instructor': - entityState = state.instructors; - break; - case 'subject': - entityState = state.subjects; - break; - default: - return; - } - - return { - searches: entityState.searches, - entityData: entityState.data - } -} - -export default connect(mapStateToProps, utils.mapDispatchToProps)(EntitySelect) diff --git a/src/components/EntitySelect.tsx b/src/components/EntitySelect.tsx new file mode 100644 index 0000000..b3230c8 --- /dev/null +++ b/src/components/EntitySelect.tsx @@ -0,0 +1,200 @@ +import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import { useAppDispatch, useAppSelector } from '../store/hooks'; +import { Dropdown, DropdownProps } from 'semantic-ui-react'; +import _ from 'lodash'; +import { fetchInstructor, fetchInstructorSearch } from '../store/slices/instructorsSlice'; +import { fetchSubject, fetchSubjectSearch } from '../store/slices/subjectsSlice'; +import { Instructor, Subject } from '../types/api'; + +type EntityType = 'instructor' | 'subject'; +type EntityKey = number | string; + +interface EntityOption { + key: EntityKey; + value: EntityKey; + text: string; +} + +interface EntityData { + isFetching?: boolean; + data?: Instructor | Subject; +} + +interface EntitySelectProps { + entityType: EntityType; + onChange?: (entityKeys: (string | number)[]) => void; + value?: (string | number)[]; +} + +const EntitySelect: React.FC = ({ + entityType, + onChange = (_entityKey: (string | number)[]): void => {}, + value = [] +}) => { + const dispatch = useAppDispatch(); + + const instructorState = useAppSelector(state => state.instructors); + const subjectState = useAppSelector(state => state.subjects); + + const entityState = entityType === 'instructor' ? instructorState : subjectState; + const searches = entityState.searches; + const entityData = entityState.data; + + const [query, setQuery] = useState(''); + const [options, setOptions] = useState([]); + const [isTyping, setIsTyping] = useState(false); + const [isFetching, setIsFetching] = useState(false); + + const performSearch = useCallback((searchQuery: string): void => { + switch (entityType) { + case 'instructor': + dispatch(fetchInstructorSearch({ query: searchQuery, page: 1 })); + return; + case 'subject': + dispatch(fetchSubjectSearch({ query: searchQuery, page: 1 })); + return; + } + }, [dispatch, entityType]); + + const requestEntity = useCallback((key: EntityKey): void => { + switch (entityType) { + case 'instructor': + dispatch(fetchInstructor(key as number)); + return; + case 'subject': + dispatch(fetchSubject(key as string)); + return; + } + }, [dispatch, entityType]); + + const entityToKey = useCallback((entity: Instructor | Subject): EntityKey => { + switch (entityType) { + case 'instructor': + return (entity as Instructor).id; + case 'subject': + return (entity as Subject).code; + } + }, [entityType]); + + const entityToOption = useCallback((key: EntityKey, entityWrapper: EntityData): EntityOption => { + if (entityWrapper.isFetching || !entityWrapper.data) { + return { + key: key, + value: key, + text: `${key} (Loading...)` + }; + } + const entity = entityWrapper.data; + switch (entityType) { + case 'instructor': + const instructor = entity as Instructor; + return { + key: instructor.id, + value: instructor.id, + text: instructor.name || '' + }; + case 'subject': + const subject = entity as Subject; + return { + key: subject.code, + value: subject.code, + text: subject.name || '' + }; + } + }, [entityType]); + + const handleChange = (_event: React.SyntheticEvent, { value: newValue }: DropdownProps): void => { + setQuery(''); + onChange(newValue as (string | number)[]); + }; + + const handleSearchChange = (_event: React.SyntheticEvent, { searchQuery }: DropdownProps): void => { + const newQuery = searchQuery as string; + setQuery(newQuery); + setIsTyping(true); + + setTimeout(() => { + setIsTyping(false); + performSearch(newQuery); + }, 500); + }; + + useEffect(() => { + const searchData = query.length >= 2 ? searches[query]?.[1] : undefined; + + const newOptions: EntityOption[] = []; + const keys = new Set(); + + for (const keyStr of Object.keys(entityData)) { + const entityWrapper = entityData[keyStr]; + if (!entityWrapper || !entityWrapper.data) continue; + const entity = entityWrapper.data; + const key = entityToKey(entity); + newOptions.push(entityToOption(key, entityWrapper)); + keys.add(key); + } + + for (const key of value) { + if (!keys.has(key)) { + newOptions.push({ + key: key, + value: key, + text: `${key} (Loading...)` + }); + requestEntity(key); + keys.add(key); + } + } + + const newIsFetching = searchData?.isFetching || false; + + let filteredOptions = newOptions; + + if (searchData && !searchData.isFetching && searchData.data?.results) { + const searchKeys = searchData.data.results.map((e: Instructor | Subject) => entityToKey(e)); + filteredOptions = newOptions.filter(o => searchKeys.includes(o.key) || value.includes(o.key)); + } else { + filteredOptions = newOptions.filter(o => value.includes(o.key)); + } + + if (!_.isEqual(options, filteredOptions)) { + setOptions(filteredOptions); + } + + if (isFetching !== newIsFetching) { + setIsFetching(newIsFetching); + } + }, [entityType, entityData, searches, value, query, entityToKey, entityToOption, requestEntity, options, isFetching]); + + const message = useMemo(() => { + if (query.length < 2) { + return 'Start typing to see results'; + } else if (isTyping || isFetching) { + return 'Searching...'; + } else { + return 'No results found'; + } + }, [query, isTyping, isFetching]); + + return ( + options} + /> + ); +}; + +export default EntitySelect; diff --git a/src/components/Explorer.js b/src/components/Explorer.js deleted file mode 100644 index 97f0481..0000000 --- a/src/components/Explorer.js +++ /dev/null @@ -1,350 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; -import utils from "../utils"; -import PropTypes from "prop-types"; -import { - Dimmer, - Header, - Icon, - Loader, - Pagination, - Popup, - Table, -} from "semantic-ui-react"; -import _ from "lodash"; -import CourseName from "./CourseName"; -import { Link } from "react-router-dom"; -import { stringify } from "qs"; -import { Row, Col } from "../components/Grid"; - -class Explorer extends Component { - static propTypes = { - entityType: PropTypes.oneOf(["instructor", "course", "subject"]).isRequired, - sort: PropTypes.oneOf(["gpa_total", "count_avg", "gpa"]), - order: PropTypes.oneOf(["asc", "desc"]), - onSortOrderChange: PropTypes.func, - onPageChange: PropTypes.func, - page: PropTypes.number, - minCountAvg: PropTypes.number, - minGpaTotal: PropTypes.number, - filterParams: PropTypes.object, - }; - - static defaultProps = { - sort: "gpa_total", - order: "desc", - onSortOrderChange: (sort, order) => {}, - onPageChange: (page) => {}, - page: 1, - minCountAvg: 0, - minGpaTotal: 0, - filterParams: {}, - }; - - componentDidMount = () => { - this.fetchData(); - }; - - componentDidUpdate = (prevProps) => { - // Refetch if any relevant props changed - const { - entityType, - page, - sort, - order, - minCountAvg, - minGpaTotal, - filterParams, - } = this.props; - const propsChanged = - prevProps.entityType !== entityType || - prevProps.page !== page || - prevProps.sort !== sort || - prevProps.order !== order || - prevProps.minCountAvg !== minCountAvg || - prevProps.minGpaTotal !== minGpaTotal || - !_.isEqual(prevProps.filterParams, filterParams); - - if (propsChanged) { - this.fetchData(); - } - }; - - fetchData = () => { - const { - entityType, - actions, - page, - sort, - order, - minCountAvg, - minGpaTotal, - filterParams, - } = this.props; - - const params = { - page, - sort, - order, - min_count_avg: minCountAvg, - min_gpa_total: minGpaTotal, - per_page: 15, - ...filterParams, - }; - - switch (entityType) { - case "course": - actions.fetchExploreCourses(params); - break; - case "instructor": - actions.fetchExploreInstructors(params); - break; - case "subject": - actions.fetchExploreSubjects(params); - break; - default: - break; - } - }; - - onPageChange = (event, data) => { - const { activePage } = data; - this.props.onPageChange(activePage); - }; - - onSortChange = (newSort) => () => { - const { sort, order, onSortOrderChange } = this.props; - - let newOrder; - - if (sort !== newSort) { - newOrder = "asc"; - } else { - newOrder = order === "asc" ? "desc" : "asc"; - } - - onSortOrderChange(newSort, newOrder); - }; - - entryKey = (entry) => { - const { entityType } = this.props; - - switch (entityType) { - case "course": - return entry.course.uuid; - case "instructor": - return entry.instructor.id; - case "subject": - return entry.subject.code; - default: - return null; - } - }; - - renderEntryName = (entry) => { - const { entityType } = this.props; - let link; - - switch (entityType) { - case "course": - const { course } = entry; - return ( -
- - - - - - - - -
- ); - case "instructor": - const { instructor } = entry; - link = "/search?" + stringify({ instructors: [instructor.id] }); - return ( -
- - {instructor.name} - -
- ); - case "subject": - const { subject } = entry; - link = "/search?" + stringify({ subjects: [subject.code] }); - return ( -
- - {subject.name} - -
- ); - default: - break; - } - }; - - renderEntries = (results) => { - if (!results) return null; - - return results.map((entry) => { - return ( - - {this.renderEntryName(entry)} - - Avg. # Grades: - {utils.numberWithCommas(parseFloat(entry.countAvg.toFixed(1)))} - - - Total # Grades: - {utils.numberWithCommas(entry.gpaTotal)} - - - Avg. GPA: - {entry.gpa.toFixed(3)} - - - ); - }); - }; - - render = () => { - const { data, entityType, sort, order, page } = this.props; - const entityName = _.upperFirst(entityType) + "s"; - - let orderFull = order === "asc" ? "ascending" : "descending"; - - let activePage = page; - let totalPages = 1; - let results; - let entries = [ - - - - - - - , - ]; - - if (data && !data.isFetching) { - totalPages = data.totalPages; - results = data.results; - entries = this.renderEntries(results); - } - - return ( - - - - {entityName} - - Avg. # Grades{" "} - }> - - The average number of students per grade distribution entry. - This is often equivalent to the average number of students per - course section. - - - - - Total # Grades{" "} - }> - - The total number of students with grades reported. - - - - - Avg. GPA{" "} - }> - - The average GPA given to students. - - - - - - - {entries} - - - - - - - , - icon: true, - }} - firstItem={null} - lastItem={null} - prevItem={{ - content: , - icon: true, - }} - nextItem={{ - content: , - icon: true, - }} - totalPages={totalPages} - size="mini" - siblingRange={1} - /> - - - - - -
- ); - }; -} - -function mapStateToProps(state, ownProps) { - const { entityType } = ownProps; - - let data; - - switch (entityType) { - case "instructor": - data = state.explore.instructors.data; - break; - case "course": - data = state.explore.courses.data; - break; - case "subject": - data = state.explore.subjects.data; - break; - default: - break; - } - - return { - data, - }; -} - -export default connect(mapStateToProps, utils.mapDispatchToProps)(Explorer); diff --git a/src/components/Explorer.tsx b/src/components/Explorer.tsx new file mode 100644 index 0000000..0ffdb77 --- /dev/null +++ b/src/components/Explorer.tsx @@ -0,0 +1,312 @@ +import React, { useEffect } from "react"; +import { useAppDispatch, useAppSelector } from "../store/hooks"; +import { + Dimmer, + Header, + Icon, + Loader, + Pagination, + PaginationProps, + Popup, + Table, +} from "semantic-ui-react"; +import _ from "lodash"; +import CourseName from "./CourseName"; +import { Link } from "react-router-dom"; +import { stringify } from "qs"; +import { Row, Col } from "../components/Grid"; +import utils from "../utils"; +import { + fetchExploreCourses, + fetchExploreInstructors, + fetchExploreSubjects, +} from "../store/slices/exploreSlice"; +import { + ExploreCourseEntry, + ExploreInstructorEntry, + ExploreSubjectEntry, +} from "../types/api"; + +export type EntityType = "instructor" | "course" | "subject"; +type ExploreEntry = ExploreCourseEntry | ExploreInstructorEntry | ExploreSubjectEntry; + +interface ExplorerProps { + entityType: EntityType; + sort?: string; + order?: string; + onSortOrderChange?: (sort: string, order: string) => void; + onPageChange?: (page: number) => void; + page?: number; + minCountAvg?: number; + minGpaTotal?: number; + filterParams?: Record; +} + +const Explorer: React.FC = ({ + entityType, + sort = "gpa_total", + order = "desc", + onSortOrderChange = (_sort: string, _order: string): void => {}, + onPageChange = (_page: number): void => {}, + page = 1, + minCountAvg = 0, + minGpaTotal = 0, + filterParams = {}, +}) => { + const dispatch = useAppDispatch(); + + const coursesData = useAppSelector(state => state.explore.courses); + const instructorsData = useAppSelector(state => state.explore.instructors); + const subjectsData = useAppSelector(state => state.explore.subjects); + + let data; + switch (entityType) { + case "instructor": + data = instructorsData; + break; + case "course": + data = coursesData; + break; + case "subject": + data = subjectsData; + break; + default: + data = coursesData; + break; + } + + // Stringify filterParams to avoid object reference issues + const filterParamsString = JSON.stringify(filterParams); + + useEffect(() => { + const params = { + page, + sort, + order, + min_count_avg: minCountAvg, + min_gpa_total: minGpaTotal, + per_page: 15, + ...JSON.parse(filterParamsString), + }; + + switch (entityType) { + case "course": + dispatch(fetchExploreCourses(params)); + break; + case "instructor": + dispatch(fetchExploreInstructors(params)); + break; + case "subject": + dispatch(fetchExploreSubjects(params)); + break; + } + }, [dispatch, entityType, page, sort, order, minCountAvg, minGpaTotal, filterParamsString]); + + const handlePageChange = (_event: React.MouseEvent, data: PaginationProps): void => { + const { activePage } = data; + onPageChange(activePage as number); + }; + + const handleSortChange = (newSort: string) => (): void => { + let newOrder: string; + + if (sort !== newSort) { + newOrder = "asc"; + } else { + newOrder = order === "asc" ? "desc" : "asc"; + } + + onSortOrderChange(newSort, newOrder); + }; + + const entryKey = (entry: ExploreEntry): string | number => { + switch (entityType) { + case "course": + return (entry as ExploreCourseEntry).course.uuid; + case "instructor": + return (entry as ExploreInstructorEntry).instructor.id; + case "subject": + return (entry as ExploreSubjectEntry).subject.code; + default: + return ''; + } + }; + + const renderEntryName = (entry: ExploreEntry) => { + let link: string; + + switch (entityType) { + case "course": + const { course } = entry as ExploreCourseEntry; + return ( +
+ + + + + + + + +
+ ); + case "instructor": + const { instructor } = entry as ExploreInstructorEntry; + link = "/search?" + stringify({ instructors: [instructor.id] }); + return ( +
+ + {instructor.name} + +
+ ); + case "subject": + const { subject } = entry as ExploreSubjectEntry; + link = "/search?" + stringify({ subjects: [subject.code] }); + return ( +
+ + {subject.name} + +
+ ); + default: + return undefined; + } + }; + + const renderEntries = (results: ExploreEntry[]) => { + if (!results) return null; + + return results.map((entry) => { + return ( + + {renderEntryName(entry)} + + Avg. # Grades: + {utils.numberWithCommas(parseFloat(entry.countAvg.toFixed(1)))} + + + Total # Grades: + {utils.numberWithCommas(entry.gpaTotal)} + + + Avg. GPA: + {entry.gpa.toFixed(3)} + + + ); + }); + }; + + const entityName = _.upperFirst(entityType) + "s"; + const orderFull = order === "asc" ? "ascending" : "descending"; + + const activePage = page; + let totalPages = 1; + let results: ExploreEntry[] | undefined; + let entries: React.ReactNode[] = [ + + + + + + + , + ]; + + if (data?.data && !data.isFetching) { + totalPages = data.data.totalPages; + results = data.data.results as ExploreEntry[]; + entries = renderEntries(results) || []; + } + + return ( + + + + {entityName} + + Avg. # Grades{" "} + }> + + The average number of students per grade distribution entry. + This is often equivalent to the average number of students per + course section. + + + + + Total # Grades{" "} + }> + + The total number of students with grades reported. + + + + + Avg. GPA{" "} + }> + + The average GPA given to students. + + + + + + + {entries} + + + + + + + , + icon: true, + }} + firstItem={null} + lastItem={null} + prevItem={{ + content: , + icon: true, + }} + nextItem={{ + content: , + icon: true, + }} + totalPages={totalPages} + size="mini" + siblingRange={1} + /> + + + + + +
+ ); +}; + +export default Explorer; diff --git a/src/components/Grid.js b/src/components/Grid.js deleted file mode 100644 index fb7f73f..0000000 --- a/src/components/Grid.js +++ /dev/null @@ -1,36 +0,0 @@ -import React from "react"; -import "../styles/components/Grid.css"; - -export const Row = ({ children, center, middle, between, ...props }) => { - const classNames = ["grid-row"]; - if (center) classNames.push("grid-row-center"); - if (middle) classNames.push("grid-row-middle"); - if (between) classNames.push("grid-row-between"); - - return ( -
- {children} -
- ); -}; - -export const Col = ({ children, xs, sm, md, lg, auto, ...props }) => { - const classNames = ["grid-col"]; - - if (xs) classNames.push(`grid-col-xs-${xs}`); - if (sm) classNames.push(`grid-col-sm-${sm}`); - if (md) classNames.push(`grid-col-md-${md}`); - if (lg) classNames.push(`grid-col-lg-${lg}`); - if (auto) classNames.push("grid-col-auto"); - - // If no size specified, default to auto - if (!xs && !sm && !md && !lg && !auto) { - classNames.push("grid-col-auto"); - } - - return ( -
- {children} -
- ); -}; diff --git a/src/components/Grid.tsx b/src/components/Grid.tsx new file mode 100644 index 0000000..a8a1fac --- /dev/null +++ b/src/components/Grid.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import "../styles/components/Grid.css"; + +interface RowProps extends React.HTMLAttributes { + children?: React.ReactNode; + center?: boolean; + middle?: boolean; + between?: boolean; +} + +export const Row: React.FC = ({ children, center, middle, between, ...props }) => { + const classNames = ["grid-row"]; + if (center) classNames.push("grid-row-center"); + if (middle) classNames.push("grid-row-middle"); + if (between) classNames.push("grid-row-between"); + + return ( +
+ {children} +
+ ); +}; + +interface ColProps extends React.HTMLAttributes { + children?: React.ReactNode; + xs?: number | boolean; + sm?: number | boolean; + md?: number; + lg?: number; + auto?: boolean; +} + +export const Col: React.FC = ({ children, xs, sm, md, lg, auto, ...props }) => { + const classNames = ["grid-col"]; + + if (typeof xs === 'number') { + classNames.push(`grid-col-xs-${xs}`); + } + if (typeof sm === 'number') { + classNames.push(`grid-col-sm-${sm}`); + } + if (md) classNames.push(`grid-col-md-${md}`); + if (lg) classNames.push(`grid-col-lg-${lg}`); + if (auto) classNames.push("grid-col-auto"); + + // If xs or sm is true (boolean), treat as flexible column + if ((xs === true || sm === true) && !auto) { + classNames.push("grid-col-auto"); + } + + // Default to auto if no sizing specified + if (!xs && !sm && !md && !lg && !auto) { + classNames.push("grid-col-auto"); + } + + return ( +
+ {children} +
+ ); +}; diff --git a/src/components/LatestTerm.js b/src/components/LatestTerm.js deleted file mode 100644 index 65dcdec..0000000 --- a/src/components/LatestTerm.js +++ /dev/null @@ -1,34 +0,0 @@ -import React, {Component} from 'react'; -import {connect} from 'react-redux'; -import utils from '../utils'; - -class LatestTerm extends Component { - componentDidMount = () => { - this.props.actions.fetchTerms(); - } - - latestTermName = () => { - const { terms } = this.props; - - if (terms) { - const latestTerm = Math.max(...Object.keys(terms).map(key => parseInt(key, 10))); - return utils.termCodes.toName(latestTerm); - } - else { - return "Unknown"; - } - } - - render = () => { - return {this.latestTermName()} - } -} - -function mapStateToProps(state, ownProps) { - return { - terms: state.app.terms || {} - } -} - - -export default connect(mapStateToProps, utils.mapDispatchToProps)(LatestTerm) \ No newline at end of file diff --git a/src/components/LatestTerm.tsx b/src/components/LatestTerm.tsx new file mode 100644 index 0000000..6c5e8b4 --- /dev/null +++ b/src/components/LatestTerm.tsx @@ -0,0 +1,27 @@ +import React, { useEffect } from 'react'; +import { useAppDispatch, useAppSelector } from '../store/hooks'; +import { fetchTerms } from '../store/slices/appSlice'; +import utils from '../utils'; + +const LatestTerm: React.FC = () => { + const dispatch = useAppDispatch(); + const terms = useAppSelector(state => state.app.terms); + + useEffect(() => { + dispatch(fetchTerms()); + }, [dispatch]); + + const latestTermName = (): string => { + if (terms && terms.length > 0) { + const latestTermCode = Math.max(...terms.map(t => t.code)); + return utils.termCodes.toName(latestTermCode); + } + else { + return "Unknown"; + } + }; + + return {latestTermName()}; +}; + +export default LatestTerm; diff --git a/src/components/SearchBox.js b/src/components/SearchBox.js deleted file mode 100644 index 67c56f0..0000000 --- a/src/components/SearchBox.js +++ /dev/null @@ -1,85 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; -import utils from "../utils"; -import { Input } from "semantic-ui-react"; -import { useNavigate } from "react-router-dom"; - -class SearchBox extends Component { - state = { - searchValue: "", - }; - - componentDidUpdate = (prevProps) => { - // when we get an outside search value update, reflect that in the - // search box via the local state - if (prevProps.searchQuery !== this.props.searchQuery) { - this.setState({ - searchValue: this.props.searchQuery, - }); - } - }; - - performSearch = () => { - const { searchValue } = this.state; - - // tell the app about the search! - this.props.navigate(`/search?query=${searchValue}`); - }; - - onInputChange = (event, data) => { - // update the state of the search box to the new value - this.setState({ - searchValue: data.value, - }); - }; - - onKeyPress = (event) => { - if (event.key === "Enter") { - this.performSearch(); - event.target.blur(); - } - }; - - render = () => { - const { searchValue } = this.state; - - return ( - - ); - }; -} - -function mapStateToProps(state) { - // we grab the app state search query, only used on page load basically - // like when you refresh the search page for some odd reason - return { - searchQuery: state.app.searchQuery, - }; -} - -// HOC to inject navigate as prop -function withNavigate(Component) { - return function ComponentWithNavigate(props) { - const navigate = useNavigate(); - return ; - }; -} - -export default connect( - mapStateToProps, - utils.mapDispatchToProps -)(withNavigate(SearchBox)); diff --git a/src/components/SearchBox.tsx b/src/components/SearchBox.tsx new file mode 100644 index 0000000..b42a3cd --- /dev/null +++ b/src/components/SearchBox.tsx @@ -0,0 +1,49 @@ +import React, { useEffect, useState } from "react"; +import { useAppSelector } from "../store/hooks"; +import { Input, InputOnChangeData } from "semantic-ui-react"; +import { useNavigate } from "react-router-dom"; + +const SearchBox: React.FC = () => { + const navigate = useNavigate(); + const searchQuery = useAppSelector(state => state.app.searchQuery); + const [searchValue, setSearchValue] = useState(""); + + useEffect(() => { + setSearchValue(searchQuery); + }, [searchQuery]); + + const performSearch = (): void => { + navigate(`/search?query=${searchValue}`); + }; + + const onInputChange = (_event: React.ChangeEvent, data: InputOnChangeData): void => { + setSearchValue(data.value); + }; + + const onKeyPress = (event: React.KeyboardEvent): void => { + if (event.key === "Enter") { + performSearch(); + (event.target as HTMLInputElement).blur(); + } + }; + + return ( + + ); +}; + +export default SearchBox; diff --git a/src/components/SearchResultCount.js b/src/components/SearchResultCount.js deleted file mode 100644 index 6cd9865..0000000 --- a/src/components/SearchResultCount.js +++ /dev/null @@ -1,33 +0,0 @@ -import React, {Component} from 'react'; -import {connect} from 'react-redux'; -import utils from '../utils'; - -class SearchResultCount extends Component { - render = () => { - const { count } = this.props; - - return {utils.numberWithCommas(count)} - } -} - -function mapStateToProps(state, ownProps) { - const { search } = state.courses; - - const { page } = state.app.courseFilterParams || 1; - - let count = search && search.pages && search.pages[page] && search.pages[page].totalCount; - - if (count) { - return { - count - } - } - else { - return { - count: 0 - } - } -} - - -export default connect(mapStateToProps, utils.mapDispatchToProps)(SearchResultCount) diff --git a/src/components/SearchResultCount.tsx b/src/components/SearchResultCount.tsx new file mode 100644 index 0000000..09b8560 --- /dev/null +++ b/src/components/SearchResultCount.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { useAppSelector } from '../store/hooks'; +import utils from '../utils'; + +const SearchResultCount: React.FC = () => { + const courseSearch = useAppSelector(state => state.courses.search); + + const page = 1; + const pageData = courseSearch?.pages?.[page]; + const count = pageData?.total || 0; + + return {utils.numberWithCommas(count)}; +}; + +export default SearchResultCount; diff --git a/src/components/SetCourseFilterParams.js b/src/components/SetCourseFilterParams.js deleted file mode 100644 index e032a44..0000000 --- a/src/components/SetCourseFilterParams.js +++ /dev/null @@ -1,30 +0,0 @@ -import { Component } from "react"; -import { connect } from "react-redux"; -import utils from "../utils"; -import PropTypes from "prop-types"; -import _ from "lodash"; - -class SetCourseFilterParams extends Component { - static propTypes = { - params: PropTypes.object, - }; - - setCourseFilterParams = () => { - const { params, actions } = this.props; - const { page } = params; - actions.setCourseFilterParams(params); - actions.fetchCourseSearch(params, page); - }; - - componentDidMount = this.setCourseFilterParams; - - componentDidUpdate = (prevProps) => { - if (!_.isEqual(prevProps.params, this.props.params)) { - this.setCourseFilterParams(); - } - }; - - render = () => null; -} - -export default connect(null, utils.mapDispatchToProps)(SetCourseFilterParams); diff --git a/src/components/SetCourseFilterParams.tsx b/src/components/SetCourseFilterParams.tsx new file mode 100644 index 0000000..a93e666 --- /dev/null +++ b/src/components/SetCourseFilterParams.tsx @@ -0,0 +1,39 @@ +import React, { useEffect, useRef } from "react"; +import { useAppDispatch } from "../store/hooks"; +import { setCourseFilterParams } from "../store/slices/appSlice"; +import { CourseFilterParams } from "../types/api"; + +interface SetCourseFilterParamsProps { + params: CourseFilterParams; +} + +const SetCourseFilterParams: React.FC = ({ params }) => { + const dispatch = useAppDispatch(); + const prevParamsStringRef = useRef(""); + + useEffect(() => { + // Normalize the params + const normalizedParams: CourseFilterParams = { + ...params, + subjects: Array.isArray(params.subjects) + ? params.subjects + : (params.subjects ? [params.subjects] : undefined), + instructors: Array.isArray(params.instructors) + ? params.instructors.map(i => typeof i === 'number' ? i : parseInt(String(i), 10)) + : (params.instructors ? [typeof params.instructors === 'number' ? params.instructors : parseInt(String(params.instructors), 10)] : undefined) + }; + + // Use JSON.stringify for comparison to avoid reference issues + const paramsString = JSON.stringify(normalizedParams); + + // Only dispatch if params actually changed + if (paramsString !== prevParamsStringRef.current) { + dispatch(setCourseFilterParams(normalizedParams)); + prevParamsStringRef.current = paramsString; + } + }, [dispatch, params]); + + return null; +}; + +export default SetCourseFilterParams; diff --git a/src/components/SubjectName.js b/src/components/SubjectName.js deleted file mode 100644 index 707b62c..0000000 --- a/src/components/SubjectName.js +++ /dev/null @@ -1,49 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; -import PropTypes from "prop-types"; -import utils from "../utils"; - -class SubjectName extends Component { - static propTypes = { - code: PropTypes.string, - abbreviate: PropTypes.bool, - fallback: PropTypes.string, - data: PropTypes.object, - }; - - componentDidMount = () => { - const { actions, code, data } = this.props; - - if (!data) { - actions.fetchSubject(code); - } - }; - - render = () => { - const { name, abbreviation, abbreviate, fallback } = this.props; - - const text = abbreviate ? abbreviation : name; - return {text || fallback}; - }; -} - -function mapStateToProps(state, ownProps) { - const { code, data } = ownProps; - - if (data) { - return { - name: data.name, - abbreviation: data.abbreviation, - }; - } - - const { subjects } = state; - const subjectData = subjects.data[code]; - - return { - name: subjectData && subjectData.name, - abbreviation: subjectData && subjectData.abbreviation, - }; -} - -export default connect(mapStateToProps, utils.mapDispatchToProps)(SubjectName); diff --git a/src/components/SubjectName.tsx b/src/components/SubjectName.tsx new file mode 100644 index 0000000..c05bd05 --- /dev/null +++ b/src/components/SubjectName.tsx @@ -0,0 +1,33 @@ +import React, { useEffect } from "react"; +import { useAppDispatch, useAppSelector } from "../store/hooks"; +import { fetchSubject } from "../store/slices/subjectsSlice"; +import { Subject } from "../types/api"; + +interface SubjectNameProps { + code?: string; + abbreviate?: boolean; + fallback?: string; + data?: Subject; +} + +const SubjectName: React.FC = ({ code, data, abbreviate, fallback }) => { + const dispatch = useAppDispatch(); + const subjectData = useAppSelector(state => + code && !data ? state.subjects.data[code]?.data : undefined + ); + + useEffect(() => { + if (code && !data) { + dispatch(fetchSubject(code)); + } + }, [code, data, dispatch]); + + const finalData = data || subjectData; + const name = finalData?.name; + const abbreviation = finalData?.abbreviation; + + const text = abbreviate ? abbreviation : name; + return {text || fallback}; +}; + +export default SubjectName; diff --git a/src/components/_ComponentTemplate.js b/src/components/_ComponentTemplate.js deleted file mode 100644 index 3f165fd..0000000 --- a/src/components/_ComponentTemplate.js +++ /dev/null @@ -1,25 +0,0 @@ -import React, {Component} from 'react'; -import {connect} from 'react-redux'; -import utils from '../utils'; -import PropTypes from 'prop-types' - -class Template extends Component { - static propTypes = { - example: PropTypes.number - }; - - componentDidMount = () => { - const { actions } = this.props; - }; - - render = () => { - return
Template
- } -} - -function mapStateToProps(state, ownProps) { - return {}; -} - - -export default connect(mapStateToProps, utils.mapDispatchToProps)(Template) diff --git a/src/components/_ComponentTemplate.tsx b/src/components/_ComponentTemplate.tsx new file mode 100644 index 0000000..dd7a005 --- /dev/null +++ b/src/components/_ComponentTemplate.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +interface TemplateProps { + example?: number; +} + +const Template: React.FC = () => { + return
Template
; +}; + +export default Template; diff --git a/src/containers/AdSlot.js b/src/containers/AdSlot.js deleted file mode 100644 index 1479c11..0000000 --- a/src/containers/AdSlot.js +++ /dev/null @@ -1,29 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; - -class AdSlot extends Component { - static propTypes = { - slot: PropTypes.string.isRequired, - adWidth: PropTypes.string, - adHeight: PropTypes.string, - }; - - componentDidMount = () => { - (window.adsbygoogle = window.adsbygoogle || []).push({}); - }; - - render = () => ( - - ); -} - -export default AdSlot; diff --git a/src/containers/AdSlot.tsx b/src/containers/AdSlot.tsx new file mode 100644 index 0000000..7fded53 --- /dev/null +++ b/src/containers/AdSlot.tsx @@ -0,0 +1,34 @@ +import React, { useEffect } from "react"; + +declare global { + interface Window { + adsbygoogle: Array>; + } +} + +interface AdSlotProps { + slot: string; + adWidth?: string; + adHeight?: string; +} + +const AdSlot: React.FC = ({ slot, adWidth, adHeight }) => { + useEffect(() => { + (window.adsbygoogle = window.adsbygoogle || []).push({}); + }, []); + + return ( + + ); +}; + +export default AdSlot; diff --git a/src/containers/ApiStatusPill.js b/src/containers/ApiStatusPill.js deleted file mode 100644 index d8f67f1..0000000 --- a/src/containers/ApiStatusPill.js +++ /dev/null @@ -1,63 +0,0 @@ -import React, { Component } from "react"; -import { Label, Icon } from "semantic-ui-react"; -import fetchStatus from "../utils/fetchStatus"; - -const statusLink = "https://stats.uptimerobot.com/pZaStpJFAt"; - -class ApiStatusPill extends Component { - state = { - uptime: undefined, - status: "N/A", - }; - - componentDidMount = () => { - fetchStatus().then((monitor) => { - if (monitor !== undefined && monitor.uptime !== undefined) { - this.setState({ - uptime: monitor.uptime, - status: monitor.status, - }); - } - }); - }; - - render = () => { - const { uptime, status } = this.state; - - const uptimePercent = - uptime === undefined ? "N/A" : uptime >= 100 ? 100 : uptime.toFixed(2); - - var icon = "thumbs down"; - var text = `${uptimePercent}% Uptime`; - var color; - - if (status === "N/A") { - text = "Unknown Status"; - color = undefined; - } else if (status === "DOWN") { - text = "API Down"; - color = "red"; - } else if (status === "SEEMS_DOWN") { - text = "API Unstable"; - color = "orange"; - } else if (uptime < 75) { - color = "red"; - } else if (uptime < 95) { - color = "orange"; - } else if (uptime < 99) { - color = "yellow"; - icon = "thumbs up"; - } else { - color = "green"; - icon = "thumbs up"; - } - - return ( - - ); - }; -} - -export default ApiStatusPill; diff --git a/src/containers/ApiStatusPill.tsx b/src/containers/ApiStatusPill.tsx new file mode 100644 index 0000000..df8600c --- /dev/null +++ b/src/containers/ApiStatusPill.tsx @@ -0,0 +1,55 @@ +import React, { useEffect, useState } from "react"; +import { Label, Icon, SemanticCOLORS } from "semantic-ui-react"; +import fetchStatus from "../utils/fetchStatus"; + +const statusLink = "https://stats.uptimerobot.com/pZaStpJFAt"; + +const ApiStatusPill: React.FC = () => { + const [uptime, setUptime] = useState(undefined); + const [status, setStatus] = useState("N/A"); + + useEffect(() => { + fetchStatus().then((monitor) => { + if (monitor !== undefined && monitor.uptime !== undefined) { + setUptime(monitor.uptime); + setStatus(monitor.status); + } + }); + }, []); + + const uptimePercent = + uptime === undefined ? "N/A" : uptime >= 100 ? 100 : uptime.toFixed(2); + + let icon: "thumbs down" | "thumbs up" = "thumbs down"; + let text = `${uptimePercent}% Uptime`; + let color: SemanticCOLORS | undefined; + + if (status === "N/A") { + text = "Unknown Status"; + color = undefined; + } else if (status === "DOWN") { + text = "API Down"; + color = "red"; + } else if (status === "SEEMS_DOWN") { + text = "API Unstable"; + color = "orange"; + } else if (uptime !== undefined && uptime < 75) { + color = "red"; + } else if (uptime !== undefined && uptime < 95) { + color = "orange"; + } else if (uptime !== undefined && uptime < 99) { + color = "yellow"; + icon = "thumbs up"; + } else { + color = "green"; + icon = "thumbs up"; + } + + return ( + + ); +}; + +export default ApiStatusPill; diff --git a/src/containers/CourseSearchResultItem.js b/src/containers/CourseSearchResultItem.tsx similarity index 83% rename from src/containers/CourseSearchResultItem.js rename to src/containers/CourseSearchResultItem.tsx index 6a59d4d..45ae475 100644 --- a/src/containers/CourseSearchResultItem.js +++ b/src/containers/CourseSearchResultItem.tsx @@ -3,14 +3,19 @@ import { Header, Segment, Button } from "semantic-ui-react"; import CourseName from "../components/CourseName"; import { Link, useLocation, useNavigate } from "react-router-dom"; import SubjectNameList from "./SubjectNameList"; +import { Course } from "../types/api"; -const CourseSearchResultItem = ({ result }) => { +interface CourseSearchResultItemProps { + result: Course; +} + +const CourseSearchResultItem: React.FC = ({ result }) => { const location = useLocation(); const navigate = useNavigate(); const params = new URLSearchParams(location.search); const compareWith = params.get("compareWith"); - const handleCompare = () => { + const handleCompare = (): void => { navigate(`/courses/${compareWith}?compareWith=${result.uuid}`); }; diff --git a/src/containers/Div.js b/src/containers/Div.js deleted file mode 100644 index a57486c..0000000 --- a/src/containers/Div.js +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; - -const Div = (props) => ( -
-); -export default Div; \ No newline at end of file diff --git a/src/containers/Div.tsx b/src/containers/Div.tsx new file mode 100644 index 0000000..e9cf885 --- /dev/null +++ b/src/containers/Div.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +const Div: React.FC> = (props) => ( +
+); + +export default Div; diff --git a/src/containers/PromoCard.js b/src/containers/PromoCard.tsx similarity index 70% rename from src/containers/PromoCard.js rename to src/containers/PromoCard.tsx index 75463b4..5333808 100644 --- a/src/containers/PromoCard.js +++ b/src/containers/PromoCard.tsx @@ -1,12 +1,14 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Card, Button, Icon, Label } from 'semantic-ui-react'; -/** - * Promotional card component for showcasing UW Madison student-created sites - */ -const PromoCard = ({ title, description, link, dateAdded }) => { - // Check if the card was added within the last 6 months +interface PromoCardProps { + title: string; + description?: string; + link: string; + dateAdded?: string; +} + +const PromoCard: React.FC = ({ title, description, link, dateAdded }) => { const isNew = dateAdded ? (() => { const addedDate = new Date(dateAdded); const sixMonthsAgo = new Date(); @@ -41,11 +43,4 @@ const PromoCard = ({ title, description, link, dateAdded }) => { ); }; -PromoCard.propTypes = { - title: PropTypes.string.isRequired, - description: PropTypes.string, - link: PropTypes.string.isRequired, - dateAdded: PropTypes.string -}; - export default PromoCard; diff --git a/src/containers/SiteFooter.js b/src/containers/SiteFooter.tsx similarity index 81% rename from src/containers/SiteFooter.js rename to src/containers/SiteFooter.tsx index 6051c93..77759d0 100644 --- a/src/containers/SiteFooter.js +++ b/src/containers/SiteFooter.tsx @@ -1,30 +1,30 @@ -import React, { Component } from "react"; +import React, { useEffect, useState } from "react"; import { Container, Divider, List, Label, Icon } from "semantic-ui-react"; import { Link } from "react-router-dom"; import { Row, Col } from "../components/Grid"; import logo from "../assets/logo-black.svg"; -import gitRevFile from "../assets/git-rev.txt"; import LatestTerm from "../components/LatestTerm"; import ApiStatusPill from "./ApiStatusPill"; const commitUrl = "https://github.com/Madgrades/madgrades.com/commit/"; -class SiteFooter extends Component { - state = { - gitRev: "", - }; +const SiteFooter: React.FC = () => { + const [gitRev, setGitRev] = useState(""); - componentDidMount = () => { - fetch(gitRevFile) + useEffect(() => { + fetch("/git-rev.txt") .then((response) => response.text()) .then((text) => { - this.setState({ - gitRev: text.split(" ")[0], - }); + const rev = text.split(" ")[0]; + setGitRev(rev || ""); + }) + .catch(() => { + // Fallback if file doesn't exist + setGitRev("dev"); }); - }; + }, []); - render = () => ( + return (
@@ -73,9 +73,9 @@ class SiteFooter extends Component { color="black" horizontal as="a" - href={`${commitUrl}${this.state.gitRev}`} + href={`${commitUrl}${gitRev}`} > - rev {this.state.gitRev || "Source"} + rev {gitRev || "Source"} @@ -83,6 +83,6 @@ class SiteFooter extends Component {
); -} +}; export default SiteFooter; diff --git a/src/containers/SiteHeader.js b/src/containers/SiteHeader.js deleted file mode 100644 index 80945f1..0000000 --- a/src/containers/SiteHeader.js +++ /dev/null @@ -1,95 +0,0 @@ -import React, { Component } from "react"; -import { Button, Container, Menu, Segment } from "semantic-ui-react"; -import { NavLink, useLocation } from "react-router-dom"; -import Div from "./Div"; -import SearchBox from "../components/SearchBox"; -import logo from "../assets/logo-white.svg"; - -class SiteHeader extends Component { - state = { - isNavToggled: false, - }; - - componentDidUpdate = (prevProps) => { - if (prevProps.location.pathname !== this.props.location.pathname) { - this.setState({ - isNavToggled: false, - }); - } - }; - - toggleNav = () => { - this.setState({ - isNavToggled: !this.state.isNavToggled, - }); - }; - - render = () => { - const { isNavToggled } = this.state; - const toggled = isNavToggled ? "toggled" : ""; - - const { pathname } = this.props.location; - - return ( - - - - - - - ); - }; -} - -// HOC to inject location as prop -function withLocation(Component) { - return function ComponentWithLocation(props) { - const location = useLocation(); - return ; - }; -} - -export default withLocation(SiteHeader); diff --git a/src/containers/SiteHeader.tsx b/src/containers/SiteHeader.tsx new file mode 100644 index 0000000..f11f4a4 --- /dev/null +++ b/src/containers/SiteHeader.tsx @@ -0,0 +1,76 @@ +import React, { useEffect, useState } from "react"; +import { Button, Container, Menu, Segment } from "semantic-ui-react"; +import { NavLink, useLocation } from "react-router-dom"; +import Div from "./Div"; +import SearchBox from "../components/SearchBox"; +import logo from "../assets/logo-white.svg"; + +const SiteHeader: React.FC = () => { + const location = useLocation(); + const [isNavToggled, setIsNavToggled] = useState(false); + + useEffect(() => { + setIsNavToggled(false); + }, [location.pathname]); + + const toggleNav = (): void => { + setIsNavToggled(!isNavToggled); + }; + + const toggled = isNavToggled ? "toggled" : ""; + const { pathname } = location; + + return ( + + + + + + + ); +}; + +export default SiteHeader; diff --git a/src/containers/SubjectNameList.js b/src/containers/SubjectNameList.js deleted file mode 100644 index c073462..0000000 --- a/src/containers/SubjectNameList.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import SubjectName from '../components/SubjectName'; - -const SubjectNameList = ({subjectCodes, subjects}) => { - const result = []; - const count = (subjectCodes || subjects).length; - let keys = []; - - if (subjectCodes) - keys = subjectCodes; - else - keys = subjects.map(s => s.code); - - for (let i = 0; i < count; i++) { - const curr = (subjectCodes || subjects)[i]; - - let divider = i < count - 1 && '/'; - result.push( - - - {divider} - - ); - } - - return result; -}; - -export default SubjectNameList; \ No newline at end of file diff --git a/src/containers/SubjectNameList.tsx b/src/containers/SubjectNameList.tsx new file mode 100644 index 0000000..b58fe4d --- /dev/null +++ b/src/containers/SubjectNameList.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import SubjectName from '../components/SubjectName'; +import { Subject } from '../types/api'; + +interface SubjectNameListProps { + subjectCodes?: string[]; + subjects?: Subject[]; +} + +const SubjectNameList: React.FC = ({ subjectCodes, subjects }) => { + const result: React.ReactNode[] = []; + const count = (subjectCodes || subjects)!.length; + let keys: string[] = []; + + if (subjectCodes) { + keys = subjectCodes; + } else { + keys = subjects!.map(s => s.code); + } + + for (let i = 0; i < count; i++) { + const curr = (subjectCodes || subjects)![i]; + + const divider = i < count - 1 && '/'; + result.push( + + + {divider} + + ); + } + + return <>{result}; +}; + +export default SubjectNameList; diff --git a/src/containers/TermSelect.js b/src/containers/TermSelect.js deleted file mode 100644 index 2b767e4..0000000 --- a/src/containers/TermSelect.js +++ /dev/null @@ -1,66 +0,0 @@ -import React, {Component} from 'react'; -import PropTypes from 'prop-types'; -import {Dropdown} from 'semantic-ui-react'; -import utils from '../utils/index'; - -class TermSelect extends Component { - static propTypes = { - termCodes: PropTypes.arrayOf(PropTypes.number).isRequired, - includeCumulative: PropTypes.bool, - cumulativeText: PropTypes.string, - onChange: PropTypes.func, - descriptions: PropTypes.object, - value: PropTypes.number - }; - - static defaultProps = { - includeCumulative: false, - cumulativeText: 'Cumulative', - onChange: (termCode) => {}, - descriptions: {} - }; - - generateOptions = () => { - const { includeCumulative, cumulativeText, descriptions } = this.props; - let cumulativeOption = []; - - if (includeCumulative) { - cumulativeOption = [ - {key: 0, value: 0, text: cumulativeText} - ]; - } - - const termOptions = this.props.termCodes.map(code => { - const desc = descriptions[code]; - return { - key: code, - value: code, - text: utils.termCodes.toName(code), - description: desc - } - }); - - return cumulativeOption.concat(termOptions); - }; - - onChange = (event, { value }) => { - this.props.onChange(value); - }; - - render = () => { - const { value } = this.props; - const options = this.generateOptions(); - - return ( - - ) - } -} - -export default TermSelect; diff --git a/src/containers/TermSelect.tsx b/src/containers/TermSelect.tsx new file mode 100644 index 0000000..682d963 --- /dev/null +++ b/src/containers/TermSelect.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { useMemo } from 'react'; +import { Dropdown, DropdownProps } from 'semantic-ui-react'; +import utils from '../utils/index'; + +interface TermSelectProps { + termCodes: number[]; + includeCumulative?: boolean; + cumulativeText?: string; + onChange?: (termCode: number) => void; + descriptions?: Record; + value?: number; +} + +interface TermOption { + key: number; + value: number; + text: string; + description?: string; +} + +const TermSelect: React.FC = ({ + termCodes, + includeCumulative = false, + cumulativeText = 'Cumulative', + onChange = (_termCode: number): void => {}, + descriptions = {}, + value +}) => { + const options = useMemo((): TermOption[] => { + let cumulativeOption: TermOption[] = []; + + if (includeCumulative) { + cumulativeOption = [ + { key: 0, value: 0, text: cumulativeText } + ]; + } + + const termOptions = termCodes.map(code => { + const desc = descriptions[code]; + return { + key: code, + value: code, + text: utils.termCodes.toName(code), + description: desc + }; + }); + + return cumulativeOption.concat(termOptions); + }, [termCodes, includeCumulative, cumulativeText, descriptions]); + + const handleChange = (_event: React.SyntheticEvent, { value: newValue }: DropdownProps): void => { + onChange(newValue as number); + }; + + return ( + + ); +}; + +export default TermSelect; diff --git a/src/containers/_SimpleContainerTemplate.js b/src/containers/_SimpleContainerTemplate.js deleted file mode 100644 index 5a48309..0000000 --- a/src/containers/_SimpleContainerTemplate.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; - -const Template = () => ( -
- Template -
-); -export default Template; \ No newline at end of file diff --git a/src/containers/_SimpleContainerTemplate.tsx b/src/containers/_SimpleContainerTemplate.tsx new file mode 100644 index 0000000..3cd5bc3 --- /dev/null +++ b/src/containers/_SimpleContainerTemplate.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +const Template: React.FC = () => ( +
+ Template +
+); + +export default Template; diff --git a/src/containers/charts/GpaChart.js b/src/containers/charts/GpaChart.js deleted file mode 100644 index 5a663c9..0000000 --- a/src/containers/charts/GpaChart.js +++ /dev/null @@ -1,61 +0,0 @@ -import React, {Component} from 'react'; -import { - CartesianGrid, - Label, - Line, - LineChart, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis -} from 'recharts'; -import PropTypes from 'prop-types'; -import utils from '../../utils'; - -export class GpaChart extends Component { - static propTypes = { - gradeDistributions: PropTypes.arrayOf(PropTypes.object).isRequired, - title: PropTypes.string - }; - - render = () => { - const { title, gradeDistributions } = this.props; - - if (!gradeDistributions) - return null; - - const data = gradeDistributions.map(gradeDistribution => { - return { - gpa: utils.grades.gpa(gradeDistribution), - termName: utils.termCodes.toName(gradeDistribution.termCode) - } - }); - - return ( -
- {title && ( -
-

- {title} -

-
- )} -
- - - - - Math.floor(Math.min(3.0, min)), max => 4.0]}> - - - utils.grades.formatGpa(gpa)}/> - - -
-
- ); - } -} - -export default GpaChart; \ No newline at end of file diff --git a/src/containers/charts/GpaChart.tsx b/src/containers/charts/GpaChart.tsx new file mode 100644 index 0000000..70dc5f3 --- /dev/null +++ b/src/containers/charts/GpaChart.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { + CartesianGrid, + Label, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis +} from 'recharts'; +import utils from '../../utils'; +import { GradeDistribution } from '../../types/api'; + +interface GpaChartProps { + gradeDistributions: Array; + title?: string; +} + +interface ChartDataPoint { + gpa: number; + termName: string; +} + +export const GpaChart: React.FC = ({ gradeDistributions, title }) => { + if (!gradeDistributions) + return null; + + const data: ChartDataPoint[] = gradeDistributions.map(gradeDistribution => { + const gpaValue = utils.grades.gpa(gradeDistribution); + return { + gpa: typeof gpaValue === 'number' ? gpaValue : 0, + termName: utils.termCodes.toName(gradeDistribution.termCode) + }; + }); + + return ( +
+ {title && ( +
+

+ {title} +

+
+ )} +
+ + + + + Math.floor(Math.min(3.0, min)), (_max: number) => 4.0]}> + + + utils.grades.formatGpa(gpa)} /> + + +
+
+ ); +}; + +export default GpaChart; diff --git a/src/containers/charts/GradeDistributionChart.js b/src/containers/charts/GradeDistributionChart.js deleted file mode 100644 index 4bc1695..0000000 --- a/src/containers/charts/GradeDistributionChart.js +++ /dev/null @@ -1,114 +0,0 @@ -import React, {Component} from 'react'; -import { - Bar, - BarChart, - Label, - LabelList, - Legend, - ResponsiveContainer, - XAxis, - YAxis -} from 'recharts'; -import PropTypes from 'prop-types'; -import utils from '../../utils'; - -const renderBarLabel = (props) => { - const { x, y, width, value } = props; - - return ( - - {value.split('\n')[0]} - {value.split('\n')[1]} - - ) -}; - -class GradeDistributionChart extends Component { - static propTypes = { - title: PropTypes.string, - primary: PropTypes.object, - primaryLabel: PropTypes.string, - secondary: PropTypes.object, - secondaryLabel: PropTypes.string - }; - - static defaultProps = { - title: 'Grade Distribution', - primary: utils.grades.zero(), - secondaryLabel: 'Secondary' - }; - - render = () => { - const { title, primary, secondary } = this.props; - let { primaryLabel, secondaryLabel } = this.props; - - if (!primaryLabel) { - if (secondary) { - primaryLabel = 'Primary'; - } - else { - primaryLabel = 'Grades Received'; - } - } - - const data = utils.grades.getGradeKeys(false).map(key => { - const name = utils.grades.keyToName(key); - - let percent, label, percentSecondary, labelSecondary; - - if (primary) { - const gradeCount = primary[key]; - const outOf = primary.total || 1; // we don't want to divide by 0 - percent = (gradeCount / outOf) * 100; - label = percent.toFixed(1) + '%\n' + utils.numberWithCommas(gradeCount); - } - - if (secondary) { - const gradeCount = secondary[key]; - const outOf = secondary.total || 1; // we don't want to divide by 0 - percentSecondary = (gradeCount / outOf) * 100; - labelSecondary = percentSecondary.toFixed(1) + '%\n' + utils.numberWithCommas(gradeCount); - } - - return { - name, - percent, - label, - percentSecondary, - labelSecondary - } - }); - - return ( -
-
-

- {title} -

-
-
- - - - - - - - - - {secondary && - - - - } - - - -
-
- ); - } -} - -export default GradeDistributionChart; \ No newline at end of file diff --git a/src/containers/charts/GradeDistributionChart.tsx b/src/containers/charts/GradeDistributionChart.tsx new file mode 100644 index 0000000..08a8ed4 --- /dev/null +++ b/src/containers/charts/GradeDistributionChart.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import { + Bar, + BarChart, + Label, + LabelList, + Legend, + ResponsiveContainer, + XAxis, + YAxis +} from 'recharts'; +import utils from '../../utils'; +import { GradeDistribution } from '../../types/api'; + +interface BarLabelProps { + x?: string | number; + y?: string | number; + width?: string | number; + value?: string | number; +} + +const renderBarLabel = (props: BarLabelProps) => { + const { x, y, width, value } = props; + const xNum = typeof x === 'number' ? x : parseFloat(String(x || 0)); + const yNum = typeof y === 'number' ? y : parseFloat(String(y || 0)); + const widthNum = typeof width === 'number' ? width : parseFloat(String(width || 0)); + const valueStr = String(value || ''); + + return ( + + {valueStr.split('\n')[0]} + {valueStr.split('\n')[1]} + + ); +}; + +interface GradeDistributionChartProps { + title?: string; + primary?: GradeDistribution; + primaryLabel?: string; + secondary?: GradeDistribution; + secondaryLabel?: string; +} + +interface ChartDataPoint { + name: string; + percent?: number; + label?: string; + percentSecondary?: number; + labelSecondary?: string; +} + +const GradeDistributionChart: React.FC = ({ + title = 'Grade Distribution', + primary = utils.grades.zero(), + secondary, + primaryLabel: propPrimaryLabel, + secondaryLabel = 'Secondary' +}) => { + let primaryLabel = propPrimaryLabel; + + if (!primaryLabel) { + if (secondary) { + primaryLabel = 'Primary'; + } + else { + primaryLabel = 'Grades Received'; + } + } + + const data: ChartDataPoint[] = utils.grades.getGradeKeys(false).map(key => { + const name = utils.grades.keyToName(key); + + let percent: number | undefined, label: string | undefined, percentSecondary: number | undefined, labelSecondary: string | undefined; + + if (primary) { + const gradeCount = primary[key] || 0; + const outOf = primary.total || 1; + percent = (gradeCount / outOf) * 100; + label = percent.toFixed(1) + '%\n' + utils.numberWithCommas(gradeCount); + } + + if (secondary) { + const gradeCount = secondary[key] || 0; + const outOf = secondary.total || 1; + percentSecondary = (gradeCount / outOf) * 100; + labelSecondary = percentSecondary.toFixed(1) + '%\n' + utils.numberWithCommas(gradeCount); + } + + return { + name, + percent, + label, + percentSecondary, + labelSecondary + }; + }); + + return ( +
+
+

+ {title} +

+
+
+ + + + + + + + + + {secondary && + + + + } + + + +
+
+ ); +}; + +export default GradeDistributionChart; diff --git a/src/index.jsx b/src/index.jsx deleted file mode 100644 index 43ee188..0000000 --- a/src/index.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "./App"; -import "normalize.css"; -import { applyMiddleware, combineReducers, createStore } from "redux"; -import { Provider } from "react-redux"; -import reducers from "./redux/reducers"; -import { thunk, withExtraArgument } from "redux-thunk"; -import utils from "./utils"; -import "semantic-ui-css/semantic.min.css"; -import "./styles/index.css"; -import logger from "redux-logger"; - -const api = utils.api.create( - import.meta.env.VITE_MADGRADES_API || "https://api.madgrades.com/", - import.meta.env.VITE_MADGRADES_API_TOKEN, -); - -const store = createStore( - combineReducers(reducers), - applyMiddleware( - withExtraArgument(api), - // logger - ), -); - -const root = ReactDOM.createRoot(document.getElementById("root")); -root.render( - - - , -); diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..e36485d --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,16 @@ +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "normalize.css"; +import { Provider } from "react-redux"; +import { store } from "./store"; +import "semantic-ui-css/semantic.min.css"; +import "./styles/index.scss"; + +const rootElement = document.getElementById("root"); +if (!rootElement) throw new Error('Failed to find the root element'); +const root = ReactDOM.createRoot(rootElement); +root.render( + + + , +); diff --git a/src/pages/About.js b/src/pages/About.tsx similarity index 95% rename from src/pages/About.js rename to src/pages/About.tsx index ce3f4b9..1a5f5e9 100644 --- a/src/pages/About.js +++ b/src/pages/About.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import {Button, Container, Divider, Icon} from 'semantic-ui-react'; +import { Button, Container, Divider, Icon } from 'semantic-ui-react'; const githubLink = 'https://github.com/Madgrades'; -const About = () => { +const About: React.FC = () => { document.title = 'About - Madgrades'; return ( @@ -56,4 +56,5 @@ const About = () => {
); }; + export default About; diff --git a/src/pages/Course.js b/src/pages/Course.tsx similarity index 59% rename from src/pages/Course.js rename to src/pages/Course.tsx index 1885ba8..914eb93 100644 --- a/src/pages/Course.js +++ b/src/pages/Course.tsx @@ -7,31 +7,39 @@ import CourseComparison from "../components/CourseComparison"; import { parse, stringify } from "qs"; import CourseData from "../components/CourseData"; import { useParams, useLocation, useNavigate } from "react-router-dom"; +import { Course as CourseType, Subject } from "../types/api"; -const Course = () => { +interface CourseDataType extends CourseType { + subjects: Subject[]; +} + +interface ChangeParams { + instructorId?: number; + termCode?: number; +} + +const Course: React.FC = () => { document.title = " - Madgrades"; - const { uuid } = useParams(); + const { uuid } = useParams<{ uuid: string }>(); const location = useLocation(); const navigate = useNavigate(); const params = parse(location.search.substr(1)); const { compareWith } = params; - let { instructorId, termCode } = params; - - instructorId = parseInt(instructorId || "0", 10); - termCode = parseInt(termCode || "0", 10); + let instructorId = parseInt((params.instructorId as string) || "0", 10); + let termCode = parseInt((params.termCode as string) || "0", 10); - const onChange = (params) => { - navigate(`/courses/${uuid}?${stringify(params)}`); + const onChange = (changeParams: ChangeParams): void => { + navigate(`/courses/${uuid}?${stringify(changeParams)}`); }; - const onCourseDataLoad = (data) => { + const onCourseDataLoad = (data: CourseDataType): void => { const { name, subjects, number } = data; - let visibleName = name || "Unknown Name"; - let title = visibleName + " - Madgrades"; + const visibleName = name || "Unknown Name"; + const title = visibleName + " - Madgrades"; let desc = subjects @@ -44,25 +52,25 @@ const Course = () => { " UW Madison course grade distribution and average GPA over time or by instructor."; document.title = title; - document - .querySelector('meta[name="description"]') - .setAttribute("content", desc); + const metaDescription = document.querySelector('meta[name="description"]'); + if (metaDescription) { + metaDescription.setAttribute("content", desc); + } }; - const handleCompare = () => { - // Navigate to search page with current course pre-selected + const handleCompare = (): void => { navigate(`/search?compareWith=${uuid}`); }; - const removeComparison = () => { + const removeComparison = (): void => { navigate(`/courses/${uuid}`); }; if (compareWith) { return ( { return ( - +
- + - +
@@ -89,10 +97,10 @@ const Course = () => { instructorId={instructorId} termCode={termCode} onChange={onChange} - uuid={uuid} + uuid={uuid!} /> - +
); }; diff --git a/src/pages/Explore.js b/src/pages/Explore.js deleted file mode 100644 index e7ceb17..0000000 --- a/src/pages/Explore.js +++ /dev/null @@ -1,279 +0,0 @@ -import React, { Component } from "react"; -import { Container, Dropdown, Grid, Header, Form } from "semantic-ui-react"; -import { Row, Col } from "../components/Grid"; -import Explorer from "../components/Explorer"; -import EntitySelect from "../components/EntitySelect"; -import { parse, stringify } from "qs"; -import { useLocation, useNavigate, useParams } from "react-router-dom"; -import _ from "lodash"; - -const entityOptions = [ - { - key: "course", - text: "Courses", - value: "course", - }, - { - key: "instructor", - text: "Instructors", - value: "instructor", - }, - { - key: "subject", - text: "Subjects", - value: "subject", - }, -]; - -class Explore extends Component { - constructor(props) { - super(props); - - // Initialize state with values from URL - const { location, match } = this.props; - const { entity } = match.params; - const params = parse(location.search.substr(1)); - - const entityType = entity || "course"; - let minAvg = entityType === "subject" ? 1 : 25; - let minTotal = entityType === "course" ? 1500 : 500; - - let filteredParams = { - page: parseInt(params.page || 1, 10), - sort: params.sort, - order: params.order, - subjects: params.subjects, - instructors: - params.instructors && params.instructors.map((s) => parseInt(s, 10)), - }; - - if (!params.instructors) { - filteredParams.minCountAvg = minAvg; - filteredParams.minGpaTotal = minTotal; - } - - this.state = { - params: filteredParams, - entityType: entityType, - }; - } - - setStateFromQueryString = (forcedQueryParams) => { - const { location, match } = this.props; - const { entity } = match.params; - const params = forcedQueryParams || parse(location.search.substr(1)); - - const entityType = entity || "course"; - let minAvg = entityType === "subject" ? 1 : 25; - let minTotal = entityType === "course" ? 1500 : 500; - - let filteredParams = { - page: parseInt(params.page || 1, 10), - sort: params.sort, - order: params.order, - subjects: params.subjects, - instructors: - params.instructors && params.instructors.map((s) => parseInt(s, 10)), - }; - - if (!params.instructors) { - filteredParams.minCountAvg = minAvg; - filteredParams.minGpaTotal = minTotal; - } - - // if we dont have new data, ignore state update - if ( - _.isEqual(filteredParams, this.state.params) && - entityType === this.state.entityType - ) - return; - - this.setState({ - params: filteredParams, - entityType, - }); - }; - - componentDidMount = () => { - document.title = "Explore UW Madison Courses - Madgrades"; - }; - - componentDidUpdate = (prevProps) => { - if ( - prevProps.location !== this.props.location || - prevProps.match !== this.props.match - ) { - this.setStateFromQueryString(); - } - }; - - onEntityChange = (event, data) => { - const { navigate } = this.props; - - // go to the entity page - navigate("/explore/" + data.value); - - // on entity change, update params to nothing - this.setStateFromQueryString({}); - }; - - updateParams = (params) => { - const { navigate } = this.props; - const { pathname } = this.props.location; - - this.setState({ - params, - }); - - navigate(pathname + "?" + stringify(params)); - }; - - onPageChange = (page) => { - const params = { - ...this.state.params, - page, - }; - - this.updateParams(params); - }; - - onSortOrderChange = (sort, order) => { - const params = { - ...this.state.params, - sort, - order, - page: 1, - }; - - this.updateParams(params); - }; - - onSubjectChange = (value) => { - const params = { - ...this.state.params, - subjects: value, - }; - - this.updateParams(params); - }; - - onInstructorChange = (value) => { - const params = { - ...this.state.params, - instructors: value, - }; - - this.updateParams(params); - }; - - render = () => { - const { - page, - sort, - order, - minCountAvg, - minGpaTotal, - subjects, - instructors, - } = this.state.params; - - const { entityType } = this.state; - - const filterParams = {}; - - if (entityType !== "subject" && subjects) { - filterParams.subjects = subjects.join(","); - } - - if (entityType !== "subject" && instructors) { - filterParams.instructors = instructors.join(","); - } - - return ( -
- -
- - Explore:{" "} - - - - Find GPA stats on courses, instructors, subjects.* - -
- - - {entityType !== "subject" && ( - -

-

- - - - -
- - )} - - {entityType !== "subject" && ( - -

-

- - - - -
- - )} -
- - -

* Some entries are omitted due to small class sizes.

-
-
- ); - }; -} - -// HOC to inject router hooks as props -function withRouter(Component) { - return function ComponentWithRouter(props) { - const location = useLocation(); - const navigate = useNavigate(); - const params = useParams(); - return ( - - ); - }; -} - -export default withRouter(Explore); diff --git a/src/pages/Explore.tsx b/src/pages/Explore.tsx new file mode 100644 index 0000000..25309fb --- /dev/null +++ b/src/pages/Explore.tsx @@ -0,0 +1,219 @@ +import React, { useEffect, useState } from "react"; +import { Container, Dropdown, Header, Form, DropdownProps } from "semantic-ui-react"; +import { Row, Col } from "../components/Grid"; +import Explorer, { EntityType } from "../components/Explorer"; +import EntitySelect from "../components/EntitySelect"; +import { parse, stringify } from "qs"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; +import _ from "lodash"; + +interface EntityOption { + key: string; + text: string; + value: string; +} + +const entityOptions: EntityOption[] = [ + { + key: "course", + text: "Courses", + value: "course", + }, + { + key: "instructor", + text: "Instructors", + value: "instructor", + }, + { + key: "subject", + text: "Subjects", + value: "subject", + }, +]; + +interface ExploreParams { + page: number; + sort?: string; + order?: string; + subjects?: string[]; + instructors?: number[]; + minCountAvg?: number; + minGpaTotal?: number; +} + +const Explore: React.FC = () => { + const location = useLocation(); + const navigate = useNavigate(); + const { entity } = useParams<{ entity?: string }>(); + + const getInitialParams = (entityType: string, searchParams?: Record): ExploreParams => { + const params = searchParams || parse(location.search.substr(1)); + const minAvg = entityType === "subject" ? 1 : 25; + const minTotal = entityType === "course" ? 1500 : 500; + + const filteredParams: ExploreParams = { + page: parseInt((params.page as string) || "1", 10), + sort: params.sort as string, + order: params.order as string, + subjects: params.subjects as string[], + instructors: params.instructors + ? (params.instructors === "" ? undefined : (params.instructors as string[]).map((s: string) => parseInt(s, 10))) + : undefined, + }; + + if (!params.instructors) { + filteredParams.minCountAvg = minAvg; + filteredParams.minGpaTotal = minTotal; + } + + return filteredParams; + }; + + const [entityType, setEntityType] = useState(entity || "course"); + const [params, setParams] = useState(getInitialParams(entity || "course")); + + useEffect(() => { + document.title = "Explore UW Madison Courses - Madgrades"; + }, []); + + useEffect(() => { + const newEntityType = entity || "course"; + const newParams = getInitialParams(newEntityType); + const paramsString = JSON.stringify(newParams); + const currentParamsString = JSON.stringify(params); + + if (paramsString !== currentParamsString || entityType !== newEntityType) { + setEntityType(newEntityType); + setParams(newParams); + } + }, [location.search, entity]); // Use location.search instead of location object + + const onEntityChange = (_event: React.SyntheticEvent, data: DropdownProps): void => { + navigate("/explore/" + data.value); + }; + + const updateParams = (newParams: ExploreParams): void => { + setParams(newParams); + navigate(location.pathname + "?" + stringify(newParams)); + }; + + const onPageChange = (page: number): void => { + updateParams({ + ...params, + page, + }); + }; + + const onSortOrderChange = (sort: string, order: string): void => { + updateParams({ + ...params, + sort, + order, + page: 1, + }); + }; + + const onSubjectChange = (value: (string | number)[]): void => { + updateParams({ + ...params, + subjects: value.map(v => String(v)), + }); + }; + + const onInstructorChange = (value: (string | number)[]): void => { + updateParams({ + ...params, + instructors: value.map(v => typeof v === 'number' ? v : parseInt(String(v), 10)), + }); + }; + + const { + page, + sort, + order, + minCountAvg, + minGpaTotal, + subjects, + instructors, + } = params; + + const filterParams: Record = {}; + + if (entityType !== "subject" && subjects) { + filterParams.subjects = subjects.join(","); + } + + if (entityType !== "subject" && instructors) { + filterParams.instructors = instructors.join(","); + } + + return ( +
+ +
+ + Explore:{" "} + + + + Find GPA stats on courses, instructors, subjects.* + +
+ + + {entityType !== "subject" && ( + +

+

+ + + + +
+ + )} + + {entityType !== "subject" && ( + +

+

+ + + + +
+ + )} +
+ + +

* Some entries are omitted due to small class sizes.

+
+
+ ); +}; + +export default Explore; diff --git a/src/pages/Home.js b/src/pages/Home.js deleted file mode 100644 index e72e2fe..0000000 --- a/src/pages/Home.js +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react'; -import {Container, Header, Divider} from 'semantic-ui-react'; -import PromoCard from '../containers/PromoCard'; - -const Home = () => { - document.title = 'UW Madison Grade Distributions - Madgrades'; - - return ( -
- -
- - Madgrades - - - UW Madison grade distribution visualizer built for students. - -
- -

- Find grade distributions for University of - Wisconsin - Madison (UW Madison) courses. Easily compare cumulative course - grade distributions to particular instructors or semesters to - get insight into a course which you are interested in taking. - Get started by searching for a course in the search bar above. -

- -

- Note that this website is not necessarily complete and may contain - bugs, misleading information, or errors in the data reported. Please - help out by {' '} - - reporting issues - - {' '} or {' '} - - contributing fixes - . -

- - - -
- - Other UW Madison Student Projects - - - Check out these helpful tools built by UW Madison students - -
- - - - - - - - -
-
- ); -}; -export default Home; \ No newline at end of file diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx new file mode 100644 index 0000000..4795e97 --- /dev/null +++ b/src/pages/Home.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { Container, Header, Divider } from 'semantic-ui-react'; +import PromoCard from '../containers/PromoCard'; + +const Home: React.FC = () => { + document.title = 'UW Madison Grade Distributions - Madgrades'; + + return ( +
+ +
+ + Madgrades + + + UW Madison grade distribution visualizer built for students. + +
+ +

+ Find grade distributions for University of + Wisconsin - Madison (UW Madison) courses. Easily compare cumulative course + grade distributions to particular instructors or semesters to + get insight into a course which you are interested in taking. + Get started by searching for a course in the search bar above. +

+ +

+ Note that this website is not necessarily complete and may contain + bugs, misleading information, or errors in the data reported. Please + help out by {' '} + + reporting issues + + {' '} or {' '} + + contributing fixes + . +

+ + + +
+ + Other UW Madison Student Projects + + + Check out these helpful tools built by UW Madison students + +
+ + + + + + + + +
+
+ ); +}; + +export default Home; diff --git a/src/pages/NotFound.js b/src/pages/NotFound.js deleted file mode 100644 index 4e79ded..0000000 --- a/src/pages/NotFound.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import {Container} from 'semantic-ui-react'; - -const NotFound = () => { - document.title = 'Not Found - Madgrades'; - - return ( - -

-

Page not found...

-
- ); -}; -export default NotFound; \ No newline at end of file diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx new file mode 100644 index 0000000..dccd8b6 --- /dev/null +++ b/src/pages/NotFound.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Container } from 'semantic-ui-react'; + +const NotFound: React.FC = () => { + document.title = 'Not Found - Madgrades'; + + return ( + +

+

Page not found...

+
+ ); +}; + +export default NotFound; diff --git a/src/pages/Search.js b/src/pages/Search.tsx similarity index 68% rename from src/pages/Search.js rename to src/pages/Search.tsx index 3ae80d8..226796e 100644 --- a/src/pages/Search.js +++ b/src/pages/Search.tsx @@ -10,24 +10,38 @@ import { Row, Col } from "../components/Grid"; import AdSlot from "../containers/AdSlot"; import { useLocation } from "react-router-dom"; -const extractParams = (location) => { +interface SearchParams { + query: string | undefined; + page: number; + subjects: string[] | undefined; + instructors: number[] | undefined; + sort: string | undefined; + order: 'asc' | 'desc' | undefined; + compareWith: string | undefined; +} + +const extractParams = (location: { search: string }): SearchParams => { const params = parse(location.search.substr(1)); - let query = params.query || null; - let page = parseInt(params.page || "1", 10); - let subjects = undefined; + const query = params.query as string || undefined; + const page = parseInt((params.page as string) || "1", 10); + + let subjects: string[] | undefined = undefined; if (params.subjects && Array.isArray(params.subjects)) { - subjects = params.subjects; + subjects = params.subjects as string[]; } - let instructors = undefined; + + let instructors: number[] | undefined = undefined; if (Array.isArray(params.instructors)) { - instructors = params.instructors.map((i) => parseInt(i, 10)); + instructors = (params.instructors as string[]).map((i) => parseInt(i, 10)); } - let order = (params.order || "").toLowerCase(); + + let order: 'asc' | 'desc' | undefined = (params.order as string || "").toLowerCase() as 'asc' | 'desc'; if (!["asc", "desc"].includes(order)) { order = undefined; } - let sort = (params.sort || "").toLowerCase(); + + let sort: string | undefined = (params.sort as string || "").toLowerCase(); if ( ![ "number", @@ -40,7 +54,8 @@ const extractParams = (location) => { ) { sort = undefined; } - let compareWith = params.compareWith || undefined; + + const compareWith = params.compareWith as string || undefined; return { query, @@ -53,7 +68,7 @@ const extractParams = (location) => { }; }; -const Courses = () => { +const Search: React.FC = () => { document.title = "Search UW Madison Courses - Madgrades"; const location = useLocation(); const params = extractParams(location); @@ -69,9 +84,9 @@ const Courses = () => {