From 2ece7096f012e8413e6515787b1e2ade1254f857 Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Thu, 11 Dec 2025 14:07:19 -0800 Subject: [PATCH 01/11] wip: add client dir with vite + react + typescript + swc --- .gitignore | 30 +- client/tekdb-client/README.md | 73 + client/tekdb-client/eslint.config.js | 23 + client/tekdb-client/index.html | 13 + client/tekdb-client/package-lock.json | 3337 ++++++++++++++++++++++ client/tekdb-client/package.json | 30 + client/tekdb-client/public/vite.svg | 1 + client/tekdb-client/src/App.css | 42 + client/tekdb-client/src/App.tsx | 35 + client/tekdb-client/src/assets/react.svg | 1 + client/tekdb-client/src/index.css | 68 + client/tekdb-client/src/main.tsx | 10 + client/tekdb-client/tsconfig.app.json | 28 + client/tekdb-client/tsconfig.json | 7 + client/tekdb-client/tsconfig.node.json | 26 + client/tekdb-client/vite.config.ts | 7 + 16 files changed, 3730 insertions(+), 1 deletion(-) create mode 100644 client/tekdb-client/README.md create mode 100644 client/tekdb-client/eslint.config.js create mode 100644 client/tekdb-client/index.html create mode 100644 client/tekdb-client/package-lock.json create mode 100644 client/tekdb-client/package.json create mode 100644 client/tekdb-client/public/vite.svg create mode 100644 client/tekdb-client/src/App.css create mode 100644 client/tekdb-client/src/App.tsx create mode 100644 client/tekdb-client/src/assets/react.svg create mode 100644 client/tekdb-client/src/index.css create mode 100644 client/tekdb-client/src/main.tsx create mode 100644 client/tekdb-client/tsconfig.app.json create mode 100644 client/tekdb-client/tsconfig.json create mode 100644 client/tekdb-client/tsconfig.node.json create mode 100644 client/tekdb-client/vite.config.ts diff --git a/.gitignore b/.gitignore index c5ba511..4bf0cfc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +### Server + # Byte-compiled / optimized / DLL files #User added TEKDB/TEKDB/local_settings.py @@ -98,4 +100,30 @@ ENV/ .ropeproject #vscode settings -.vscode/ \ No newline at end of file +.vscode/ + +### Client +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? \ No newline at end of file diff --git a/client/tekdb-client/README.md b/client/tekdb-client/README.md new file mode 100644 index 0000000..4dcad1f --- /dev/null +++ b/client/tekdb-client/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is currently not compatible with SWC. See [this issue](https://github.com/vitejs/vite-plugin-react/issues/428) for tracking the progress. + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/client/tekdb-client/eslint.config.js b/client/tekdb-client/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/client/tekdb-client/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/client/tekdb-client/index.html b/client/tekdb-client/index.html new file mode 100644 index 0000000..03f97f0 --- /dev/null +++ b/client/tekdb-client/index.html @@ -0,0 +1,13 @@ + + + + + + + tekdb-client + + +
+ + + diff --git a/client/tekdb-client/package-lock.json b/client/tekdb-client/package-lock.json new file mode 100644 index 0000000..d0ab146 --- /dev/null +++ b/client/tekdb-client/package-lock.json @@ -0,0 +1,3337 @@ +{ + "name": "tekdb-client", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tekdb-client", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react-swc": "^4.2.2", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.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/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/core": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.3.tgz", + "integrity": "sha512-Qd8eBPkUFL4eAONgGjycZXj1jFCBW8Fd+xF0PzdTlBCWQIV1xnUT7B93wUANtW3KGjl3TRcOyxwSx/u/jyKw/Q==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.3", + "@swc/core-darwin-x64": "1.15.3", + "@swc/core-linux-arm-gnueabihf": "1.15.3", + "@swc/core-linux-arm64-gnu": "1.15.3", + "@swc/core-linux-arm64-musl": "1.15.3", + "@swc/core-linux-x64-gnu": "1.15.3", + "@swc/core-linux-x64-musl": "1.15.3", + "@swc/core-win32-arm64-msvc": "1.15.3", + "@swc/core-win32-ia32-msvc": "1.15.3", + "@swc/core-win32-x64-msvc": "1.15.3" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.3.tgz", + "integrity": "sha512-AXfeQn0CvcQ4cndlIshETx6jrAM45oeUrK8YeEY6oUZU/qzz0Id0CyvlEywxkWVC81Ajpd8TQQ1fW5yx6zQWkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.3.tgz", + "integrity": "sha512-p68OeCz1ui+MZYG4wmfJGvcsAcFYb6Sl25H9TxWl+GkBgmNimIiRdnypK9nBGlqMZAcxngNPtnG3kEMNnvoJ2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.3.tgz", + "integrity": "sha512-Nuj5iF4JteFgwrai97mUX+xUOl+rQRHqTvnvHMATL/l9xE6/TJfPBpd3hk/PVpClMXG3Uvk1MxUFOEzM1JrMYg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.3.tgz", + "integrity": "sha512-2Nc/s8jE6mW2EjXWxO/lyQuLKShcmTrym2LRf5Ayp3ICEMX6HwFqB1EzDhwoMa2DcUgmnZIalesq2lG3krrUNw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.3.tgz", + "integrity": "sha512-j4SJniZ/qaZ5g8op+p1G9K1z22s/EYGg1UXIb3+Cg4nsxEpF5uSIGEE4mHUfA70L0BR9wKT2QF/zv3vkhfpX4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.3.tgz", + "integrity": "sha512-aKttAZnz8YB1VJwPQZtyU8Uk0BfMP63iDMkvjhJzRZVgySmqt/apWSdnoIcZlUoGheBrcqbMC17GGUmur7OT5A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.3.tgz", + "integrity": "sha512-oe8FctPu1gnUsdtGJRO2rvOUIkkIIaHqsO9xxN0bTR7dFTlPTGi2Fhk1tnvXeyAvCPxLIcwD8phzKg6wLv9yug==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.3.tgz", + "integrity": "sha512-L9AjzP2ZQ/Xh58e0lTRMLvEDrcJpR7GwZqAtIeNLcTK7JVE+QineSyHp0kLkO1rttCHyCy0U74kDTj0dRz6raA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.3.tgz", + "integrity": "sha512-B8UtogMzErUPDWUoKONSVBdsgKYd58rRyv2sHJWKOIMCHfZ22FVXICR4O/VwIYtlnZ7ahERcjayBHDlBZpR0aw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.3.tgz", + "integrity": "sha512-SpZKMR9QBTecHeqpzJdYEfgw30Oo8b/Xl6rjSzBt1g0ZsXyy60KLXrp6IagQyfTYqNYE/caDvwtF2FPn7pomog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.3.tgz", + "integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, + "license": "MIT", + "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/@typescript-eslint/eslint-plugin": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz", + "integrity": "sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/type-utils": "8.49.0", + "@typescript-eslint/utils": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.49.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.49.0.tgz", + "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.49.0.tgz", + "integrity": "sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.49.0", + "@typescript-eslint/types": "^8.49.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.49.0.tgz", + "integrity": "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.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.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.49.0.tgz", + "integrity": "sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==", + "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.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz", + "integrity": "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/utils": "8.49.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.49.0.tgz", + "integrity": "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==", + "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.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.49.0.tgz", + "integrity": "sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.49.0", + "@typescript-eslint/tsconfig-utils": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.49.0.tgz", + "integrity": "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.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.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz", + "integrity": "sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.49.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/@vitejs/plugin-react-swc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.2.2.tgz", + "integrity": "sha512-x+rE6tsxq/gxrEJN3Nv3dIV60lFflPj94c90b+NNo6n1QV1QQUTLoL0MpaOVasUZ0zqVBn7ead1B5ecx1JAGfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.47", + "@swc/core": "^1.13.5" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7" + } + }, + "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", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz", + "integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "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/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "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.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/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/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/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/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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/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/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/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/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.2.tgz", + "integrity": "sha512-BdOGOY8OKRBcgoDkwqA8Q5XvOIhoNx/Sh6BnGJlet2Abt0X5BK0BDrqGyQgLhAVjD2nAg5f6o01u/OPUhG022Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.2.tgz", + "integrity": "sha512-fhyD2BLrew6qYf4NNtHff1rLXvzR25rq49p+FeqByOazc6TcSi2n8EYulo5C1PbH+1uBW++5S1SG7FcUU6mlDg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.2" + } + }, + "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/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.49.0.tgz", + "integrity": "sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.49.0", + "@typescript-eslint/parser": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/utils": "8.49.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/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.2.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz", + "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/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/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/client/tekdb-client/package.json b/client/tekdb-client/package.json new file mode 100644 index 0000000..9e12d17 --- /dev/null +++ b/client/tekdb-client/package.json @@ -0,0 +1,30 @@ +{ + "name": "tekdb-client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react-swc": "^4.2.2", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/client/tekdb-client/public/vite.svg b/client/tekdb-client/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/client/tekdb-client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/tekdb-client/src/App.css b/client/tekdb-client/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/client/tekdb-client/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/client/tekdb-client/src/App.tsx b/client/tekdb-client/src/App.tsx new file mode 100644 index 0000000..3d7ded3 --- /dev/null +++ b/client/tekdb-client/src/App.tsx @@ -0,0 +1,35 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from '/vite.svg' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + + return ( + <> +
+ + Vite logo + + + React logo + +
+

Vite + React

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ) +} + +export default App diff --git a/client/tekdb-client/src/assets/react.svg b/client/tekdb-client/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/client/tekdb-client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/tekdb-client/src/index.css b/client/tekdb-client/src/index.css new file mode 100644 index 0000000..08a3ac9 --- /dev/null +++ b/client/tekdb-client/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/client/tekdb-client/src/main.tsx b/client/tekdb-client/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/client/tekdb-client/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/client/tekdb-client/tsconfig.app.json b/client/tekdb-client/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/client/tekdb-client/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/client/tekdb-client/tsconfig.json b/client/tekdb-client/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/client/tekdb-client/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/client/tekdb-client/tsconfig.node.json b/client/tekdb-client/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/client/tekdb-client/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/client/tekdb-client/vite.config.ts b/client/tekdb-client/vite.config.ts new file mode 100644 index 0000000..2328e17 --- /dev/null +++ b/client/tekdb-client/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react-swc' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) From e9f6cbc04bec8b802df50a6c96dad08e6ba7e6c2 Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Tue, 16 Dec 2025 16:02:53 -0800 Subject: [PATCH 02/11] wip: expose explore views via drf wip: access explore apis in react frontend --- TEKDB/TEKDB/settings.py | 1 + TEKDB/entrypoint.sh | 3 +- TEKDB/explore/API/serializers.py | 8 + TEKDB/explore/API/views.py | 422 ++++++++++++++++++ TEKDB/explore/urls.py | 12 + TEKDB/explore/views.py | 1 + TEKDB/media/__init__.py | 0 TEKDB/requirements.txt | 1 + client/{tekdb-client => }/README.md | 0 client/{tekdb-client => }/eslint.config.js | 0 client/{tekdb-client => }/index.html | 0 client/{tekdb-client => }/package-lock.json | 0 client/{tekdb-client => }/package.json | 0 client/{tekdb-client => }/public/vite.svg | 0 client/{tekdb-client => }/src/App.css | 0 client/src/App.tsx | 67 +++ client/src/api/pageContent.ts | 16 + .../{tekdb-client => }/src/assets/react.svg | 0 client/{tekdb-client => }/src/index.css | 0 client/{tekdb-client => }/src/main.tsx | 0 client/tekdb-client/src/App.tsx | 35 -- client/tekdb-client/vite.config.ts | 7 - client/{tekdb-client => }/tsconfig.app.json | 0 client/{tekdb-client => }/tsconfig.json | 0 client/{tekdb-client => }/tsconfig.node.json | 0 client/vite.config.ts | 17 + 26 files changed, 547 insertions(+), 43 deletions(-) create mode 100644 TEKDB/explore/API/serializers.py create mode 100644 TEKDB/explore/API/views.py mode change 100755 => 100644 TEKDB/media/__init__.py rename client/{tekdb-client => }/README.md (100%) rename client/{tekdb-client => }/eslint.config.js (100%) rename client/{tekdb-client => }/index.html (100%) rename client/{tekdb-client => }/package-lock.json (100%) rename client/{tekdb-client => }/package.json (100%) rename client/{tekdb-client => }/public/vite.svg (100%) rename client/{tekdb-client => }/src/App.css (100%) create mode 100644 client/src/App.tsx create mode 100644 client/src/api/pageContent.ts rename client/{tekdb-client => }/src/assets/react.svg (100%) rename client/{tekdb-client => }/src/index.css (100%) rename client/{tekdb-client => }/src/main.tsx (100%) delete mode 100644 client/tekdb-client/src/App.tsx delete mode 100644 client/tekdb-client/vite.config.ts rename client/{tekdb-client => }/tsconfig.app.json (100%) rename client/{tekdb-client => }/tsconfig.json (100%) rename client/{tekdb-client => }/tsconfig.node.json (100%) create mode 100644 client/vite.config.ts diff --git a/TEKDB/TEKDB/settings.py b/TEKDB/TEKDB/settings.py index f6f4e8c..d06be2c 100644 --- a/TEKDB/TEKDB/settings.py +++ b/TEKDB/TEKDB/settings.py @@ -70,6 +70,7 @@ "Relationships", "reversion", "django.contrib.sites", + "rest_framework" # 'moderation.apps.SimpleModerationConfig', ] diff --git a/TEKDB/entrypoint.sh b/TEKDB/entrypoint.sh index 166c537..5829e0c 100644 --- a/TEKDB/entrypoint.sh +++ b/TEKDB/entrypoint.sh @@ -34,4 +34,5 @@ echo "Starting uWSGI (HTTP) on :8000" # Use HTTP socket so direct HTTP clients (browsers) can connect to the container port. # If you proxy with nginx using the uwsgi protocol, switch back to --socket and use # uwsgi_pass in nginx configuration. -uwsgi --http :8000 --master --enable-threads --module TEKDB.wsgi \ No newline at end of file +# uwsgi --http :8000 --master --enable-threads --module TEKDB.wsgi +python manage.py runserver 0.0.0.0:8000 \ No newline at end of file diff --git a/TEKDB/explore/API/serializers.py b/TEKDB/explore/API/serializers.py new file mode 100644 index 0000000..f2ca9de --- /dev/null +++ b/TEKDB/explore/API/serializers.py @@ -0,0 +1,8 @@ +from ..models import PageContent +from rest_framework import serializers + +class PageContentSerializer(serializers.ModelSerializer): + class Meta: + model = PageContent + fields = ['page', 'content', 'is_html', 'html_content'] + read_only_fields = ['page'] # Make 'page' read-only to prevent changes diff --git a/TEKDB/explore/API/views.py b/TEKDB/explore/API/views.py new file mode 100644 index 0000000..70f8848 --- /dev/null +++ b/TEKDB/explore/API/views.py @@ -0,0 +1,422 @@ +from django.http import Http404, HttpResponse +from rest_framework import permissions, viewsets, status +from rest_framework.response import Response +from rest_framework.views import APIView + +from .serializers import PageContentSerializer +from ..models import PageContent + + +class PageContentViewSet(viewsets.ModelViewSet): + """ + CRUD for PageContent via DRF. + """ + serializer_class = PageContentSerializer + queryset = PageContent.objects.all() + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + +# ----- Utility functions ported from template views ----- +def _get_project_geography(): + from TEKDB.settings import DATABASE_GEOGRAPHY + return DATABASE_GEOGRAPHY + + +def _get_model_by_type(model_type): + from TEKDB import models as tekmodels + + searchable_models = { + "resources": [tekmodels.Resources], + "places": [tekmodels.Places], + "locality": [tekmodels.Locality], + "sources": [tekmodels.Citations], + "citations": [tekmodels.Citations], + "media": [tekmodels.Media], + "activities": [tekmodels.ResourcesActivityEvents], + "relationships": [ + tekmodels.LocalityPlaceResourceEvent, + tekmodels.MediaCitationEvents, + tekmodels.PlacesCitationEvents, + tekmodels.PlacesMediaEvents, + tekmodels.PlacesResourceCitationEvents, + tekmodels.PlacesResourceEvents, + tekmodels.PlacesResourceMediaEvents, + tekmodels.ResourceActivityCitationEvents, + tekmodels.ResourceActivityMediaEvents, + tekmodels.ResourceResourceEvents, + tekmodels.ResourcesCitationEvents, + tekmodels.ResourcesMediaEvents, + ], + "localityplaceresourceevents": [tekmodels.LocalityPlaceResourceEvent], + "mediacitationevents": [tekmodels.MediaCitationEvents], + "placescitationevents": [tekmodels.PlacesCitationEvents], + "placesmediaevents": [tekmodels.PlacesMediaEvents], + "placesresourcecitationevents": [tekmodels.PlacesResourceCitationEvents], + "placesresourceevents": [tekmodels.PlacesResourceEvents], + "placesresourcemediaevents": [tekmodels.PlacesResourceMediaEvents], + "resourceactivitycitationevents": [tekmodels.ResourceActivityCitationEvents], + "resourceactivitymediaevents": [tekmodels.ResourceActivityMediaEvents], + "resourceresourceevents": [tekmodels.ResourceResourceEvents], + "resourcesactivityevents": [tekmodels.ResourcesActivityEvents], + "resourcescitationevents": [tekmodels.ResourcesCitationEvents], + "resourcesmediaevents": [tekmodels.ResourcesMediaEvents], + "people": [tekmodels.People], + } + + if model_type.lower() in searchable_models.keys(): + return searchable_models[model_type.lower()] + elif model_type.lower() == "all": + return sum( + [ + searchable_models[key] + for key in ["resources", "places", "sources", "media", "activities"] + ], + [], + ) + else: + return [] + + +# ----- Page content endpoints ----- +class PageContentSingle(APIView): + """Return single page content by name (Home/About/Help).""" + permission_classes = [permissions.AllowAny] + + def get(self, request, name: str): + try: + page_content_obj = PageContent.objects.get(page=name) + page_content = ( + page_content_obj.html_content if page_content_obj.is_html else page_content_obj.content + ) + except Exception: + page_content = f"

{name}

Set {name} Page Content In Admin

" + + return Response( + { + "page": name.lower(), + "pageTitle": name, + "pageContent": page_content, + } + ) + + +# ----- Explore/search endpoints ----- +class ExploreSearch(APIView): + permission_classes = [permissions.IsAuthenticated] + + def get_verbose_field_name(self, model, field_path): + parts = field_path.split("__") + field = None + current_model = model + for part in parts: + field = current_model._meta.get_field(part) + if hasattr(field, "related_model") and field.related_model: + current_model = field.related_model + return field.verbose_name.title() if field else field_path.replace("_", " ").title() + + def remove_match_prefix(self, string): + if string and string.startswith("match_"): + return string[6:] + return string + + def find_match_attributes(self, obj): + return [attr for attr in dir(obj) if "match" in attr] + + def get_greatest_similarity_attribute(self, result, pks): + greatest_similarity_attribute = None + matching_attributes = [] + for match_attr in self.find_match_attributes(result): + match_value = getattr(result, match_attr) + if match_value is not None and match_value == result.similarity: + matching_attributes.append(match_attr) + if len(matching_attributes) == 1: + greatest_similarity_attribute = matching_attributes[0] + elif len(matching_attributes) > 0: + num_same_id = pks.get(result.pk, 1) + if num_same_id - 1 < len(matching_attributes): + greatest_similarity_attribute = matching_attributes[num_same_id - 1] + pks[result.pk] = max(0, num_same_id - 1) + else: + greatest_similarity_attribute = matching_attributes[0] + return greatest_similarity_attribute + + def get_results(self, keyword_string, categories): + if keyword_string is None: + keyword_string = "" + resultlist = [] + for category in categories: + query_models = _get_model_by_type(category) + for model in query_models: + model_results = model.keyword_search(keyword_string) + pks = {} + for result in model_results: + if hasattr(result, "pk"): + pks[result.pk] = pks.get(result.pk, 0) + 1 + for result in model_results: + greatest_attr = self.get_greatest_similarity_attribute(result, pks) + actual_attribute = self.remove_match_prefix(greatest_attr) if greatest_attr else None + verbose_name = ( + self.get_verbose_field_name(model, actual_attribute) if actual_attribute else None + ) + headline_key = f"headline_{actual_attribute}" if actual_attribute else None + headline_value = getattr(result, headline_key) if headline_key and hasattr(result, headline_key) else None + result_json = result.get_response_format() + if keyword_string != "": + result_json["rank"] = result.rank + result_json["similarity"] = result.similarity + result_json["headline"] = ( + f"

{verbose_name}: {headline_value}

" + if headline_value and verbose_name + else None + ) + else: + result_json["rank"] = 0 + result_json["similarity"] = 0 + result_json["headline"] = None + resultlist.append(result_json) + return sorted(resultlist, key=lambda res: (res["rank"], res["similarity"]), reverse=True) + + def get(self, request): + all_categories = ["places", "resources", "activities", "sources", "media"] + query_string = request.GET.get("query") + categories = request.GET.getlist("category") or [request.GET.get("category")] if request.GET.get("category") else [] + if categories == []: + # derive from flags or default to all + for key in all_categories: + if request.GET.get(key) == "true": + categories.append(key) + if categories == []: + categories = ["all"] + # sanitize categories + for category in categories: + if category not in all_categories and category != "all": + categories = all_categories + break + + results = self.get_results(query_string, categories) + + # cap results by config + try: + from configuration.models import Configuration + max_results = Configuration.objects.all()[0].max_results_returned + except Exception: + from TEKDB.settings import DEFAULT_MAXIMUM_RESULTS + max_results = DEFAULT_MAXIMUM_RESULTS + + too_many = len(results) > max_results + if too_many: + results = results[:max_results] + + geo = _get_project_geography() + return Response( + { + "results": results, + "categories": categories, + "query": query_string, + "too_many_results": too_many, + "map": { + "default_lon": geo["default_lon"], + "default_lat": geo["default_lat"], + "default_zoom": geo["default_zoom"], + "min_zoom": geo["min_zoom"], + "max_zoom": geo["max_zoom"], + "extent": geo["map_extent"], + }, + } + ) + + +class RecordDetail(APIView): + permission_classes = [permissions.IsAuthenticated] + + def get(self, request, model_type, id): + + models = _get_model_by_type(model_type) + if len(models) != 1: + return Response({"error": f"Incorrect number of models for {model_type}"}, status=status.HTTP_400_BAD_REQUEST) + model = models[0] + try: + obj = model.objects.get(pk=int(id)) + record_dict = obj.get_record_dict(request.user, 3857) + except Exception: + raise Http404 + + geo_info_needed = False + if ("map" in record_dict and record_dict["map"] is not None) or any( + rel.get("key") == "Place-Resource Events" and any( + (val.get("map") is not None) for val in rel.get("value", []) + ) + for rel in record_dict.get("relationships", []) + ): + geo_info_needed = True + + payload = {"record": record_dict, "model": model_type, "id": id} + if geo_info_needed: + geo = _get_project_geography() + payload["map"] = { + "default_lon": geo["default_lon"], + "default_lat": geo["default_lat"], + "default_zoom": geo["default_zoom"], + "min_zoom": geo["min_zoom"], + "max_zoom": geo["max_zoom"], + "extent": geo["map_extent"], + } + return Response(payload) + + +class ExportRecord(APIView): + permission_classes = [permissions.IsAuthenticated] + + def get(self, request, model_type, id, format): + models = _get_model_by_type(model_type) + if len(models) != 1: + return Response({"error": f"Incorrect number of models for {model_type}"}, status=status.HTTP_400_BAD_REQUEST) + model = models[0] + try: + obj = model.objects.get(pk=id) + record_dict = obj.get_record_dict(request.user, 4326) + except Exception as e: + return Response({"error": "unknown error", "code": f"{e}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + filename = f"{model_type}_{id}_{record_dict.get('name','record')}" + if format == "xls": + # Reuse original XLS export logic + import io + from xlsxwriter.workbook import Workbook + + output = io.BytesIO() + workbook = Workbook(output, {"in_membory": True}) + worksheet = workbook.add_worksheet() + workbook.add_format({"bold": True}) + row = 0 + # helper for ordered keys + def get_sorted_keys(keys): + ordered = [] + for key in [ + "name", + "image", + "subtitle", + "data", + "relationships", + "map", + "link", + "enteredbyname", + "enteredbydate", + "modifiedbyname", + "modifiedbydate", + ]: + if key in keys: + keys.remove(key) + ordered.append(key) + return ordered + keys + + for key in get_sorted_keys(list(record_dict.keys())): + field = record_dict[key] + if isinstance(field, list) and len(field) > 0 and isinstance(field[0], dict): + for item in field: + if ("key" in item and "value" in item and len(item.keys()) == 2): + if isinstance(item["value"], list) and len(item["value"]) > 0: + for sub_item in item["value"]: + worksheet.write(row, 0, f"{key} - {item['key']}") + worksheet.write(row, 1, sub_item.get("name", str(sub_item))) + row += 1 + else: + worksheet.write(row, 0, f"{key} - {item['key']}") + try: + worksheet.write(row, 1, str(item["value"])) + except Exception: + pass + row += 1 + else: + for list_key in item.keys(): + worksheet.write(row, 0, f"{key} - {list_key}") + worksheet.write(row, 1, item[list_key]) + row += 1 + else: + worksheet.write(row, 0, key) + worksheet.write(row, 1, str(field)) + row += 1 + workbook.close() + output.seek(0) + resp = HttpResponse( + output.read(), + content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ) + resp["Content-Disposition"] = f'attachment; filename="{filename}.xlsx"' + return resp + else: + # CSV default + import csv + csv_response = HttpResponse(content_type="text/csv") + csv_response["Content-Disposition"] = f'attachment; filename="{filename}.csv"' + writer = csv.writer(csv_response) + + def get_sorted_keys(keys): + ordered = [] + for key in [ + "name", + "image", + "subtitle", + "data", + "relationships", + "map", + "link", + "enteredbyname", + "enteredbydate", + "modifiedbyname", + "modifiedbydate", + ]: + if key in keys: + keys.remove(key) + ordered.append(key) + return ordered + keys + + for key in get_sorted_keys(list(record_dict.keys())): + field = record_dict[key] + if isinstance(field, list) and len(field) > 0 and isinstance(field[0], dict): + for item in field: + if ("key" in item and "value" in item and len(item.keys()) == 2): + if isinstance(item["value"], list) and len(item["value"]) > 0: + for sub_item in item["value"]: + if isinstance(sub_item, dict) and "name" in sub_item: + writer.writerow([f"{key} - {item['key']}", sub_item["name"]]) + else: + writer.writerow([f"{key} - {item['key']}", str(sub_item)]) + else: + writer.writerow([f"{key} - {item['key']}", item["value"]]) + else: + for list_key in item.keys(): + writer.writerow([f"{key} - {list_key}", item[list_key]]) + else: + writer.writerow([key, str(field)]) + return csv_response + + +class DownloadMediaFile(APIView): + permission_classes = [permissions.IsAuthenticated] + + def get(self, request, model_type, id): + models = _get_model_by_type(model_type) + if len(models) != 1: + raise Http404 + model = models[0] + try: + obj = model.objects.get(pk=id) + except Exception: + raise Http404 + media = obj.media() + import os + from TEKDB.settings import MEDIA_ROOT + + file_path = os.path.join(MEDIA_ROOT, media["file"]) + if os.path.exists(file_path): + with open(file_path, "rb") as fh: + response = HttpResponse( + fh.read(), content_type="application/force-download" + ) + response["Content-Disposition"] = ( + f"attachment; filename={os.path.basename(file_path)}" + ) + return response + raise Http404 + diff --git a/TEKDB/explore/urls.py b/TEKDB/explore/urls.py index d66626f..44bc7be 100644 --- a/TEKDB/explore/urls.py +++ b/TEKDB/explore/urls.py @@ -1,7 +1,12 @@ from django.urls import include, path, re_path +from rest_framework import routers +from .API import views as api_views from . import views +router = routers.DefaultRouter() +router.register(r"pagecontent", api_views.PageContentViewSet) + explore_patterns = [ path("", views.explore), re_path(r"^(?P\w+)/$", views.get_by_model_type), @@ -24,6 +29,13 @@ path("explore/", include(explore_patterns)), path("export", views.download), path("export/", include(export_patterns)), + # API endpoints (DRF) + path("api/pagecontent/", include(router.urls)), + re_path(r"^api/page/(?P\w+)/$", api_views.PageContentSingle.as_view()), + path("api/search/", api_views.ExploreSearch.as_view()), + re_path(r"^api/record/(?P\w+)/(?P\w+)/$", api_views.RecordDetail.as_view()), + re_path(r"^api/export/(?P\w+)/(?P\w+)/(?P\w+)/$", api_views.ExportRecord.as_view()), + re_path(r"^api/media/(?P\w+)/(?P\w+)/download$", api_views.DownloadMediaFile.as_view()), path("", views.home), ] # url(r'^logout$', views.logout, name='logout'), diff --git a/TEKDB/explore/views.py b/TEKDB/explore/views.py index 24c547f..313746f 100644 --- a/TEKDB/explore/views.py +++ b/TEKDB/explore/views.py @@ -740,3 +740,4 @@ def download(request): for row in rows: writer.writerow(row) return csv_response + diff --git a/TEKDB/media/__init__.py b/TEKDB/media/__init__.py old mode 100755 new mode 100644 diff --git a/TEKDB/requirements.txt b/TEKDB/requirements.txt index 276851e..d9b7b5c 100644 --- a/TEKDB/requirements.txt +++ b/TEKDB/requirements.txt @@ -14,6 +14,7 @@ pillow psycopg2-binary psutil django-filebrowser-no-grappelli>=4.0.0,<5.0.0 +djangorestframework XlsxWriter #-e git+https://github.com/dominno/django-moderation.git@master#egg=moderation diff --git a/client/tekdb-client/README.md b/client/README.md similarity index 100% rename from client/tekdb-client/README.md rename to client/README.md diff --git a/client/tekdb-client/eslint.config.js b/client/eslint.config.js similarity index 100% rename from client/tekdb-client/eslint.config.js rename to client/eslint.config.js diff --git a/client/tekdb-client/index.html b/client/index.html similarity index 100% rename from client/tekdb-client/index.html rename to client/index.html diff --git a/client/tekdb-client/package-lock.json b/client/package-lock.json similarity index 100% rename from client/tekdb-client/package-lock.json rename to client/package-lock.json diff --git a/client/tekdb-client/package.json b/client/package.json similarity index 100% rename from client/tekdb-client/package.json rename to client/package.json diff --git a/client/tekdb-client/public/vite.svg b/client/public/vite.svg similarity index 100% rename from client/tekdb-client/public/vite.svg rename to client/public/vite.svg diff --git a/client/tekdb-client/src/App.css b/client/src/App.css similarity index 100% rename from client/tekdb-client/src/App.css rename to client/src/App.css diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..438e3e2 --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,67 @@ +import { useEffect, useMemo, useState } from 'react' +import './App.css' +import { fetchPageContent, type PageContentResponse } from './api/pageContent' + +function App() { + const [pageName, setPageName] = useState<'Welcome' | 'About' | 'Help'>('Welcome') + const [data, setData] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const controller = useMemo(() => new AbortController(), [pageName]) + + useEffect(() => { + setLoading(true) + setError(null) + setData(null) + fetchPageContent(pageName, controller.signal) + .then((resp) => setData(resp)) + .catch((e) => { + if (e.name !== 'AbortError') setError(String(e)) + }) + .finally(() => setLoading(false)) + return () => controller.abort() + }, [pageName]) + + return ( + <> +
+

TEKDB

+ +
+ +
+ {loading &&

Loading…

} + {error &&

Error: {error}

} + {!loading && !error && data && ( +
+

{data.pageTitle}

+ {/* pageContent may include HTML from admin; rendering as HTML intentionally */} +
+
+ )} +
+ + ) +} + +export default App diff --git a/client/src/api/pageContent.ts b/client/src/api/pageContent.ts new file mode 100644 index 0000000..9ae076f --- /dev/null +++ b/client/src/api/pageContent.ts @@ -0,0 +1,16 @@ +export type PageContentResponse = { + page: string; + pageTitle: string; + pageContent: string; // May contain HTML +}; + +const baseUrl = (import.meta.env.VITE_API_BASE_URL as string) || ''; + +export async function fetchPageContent(name: string, signal?: AbortSignal): Promise { + const url = `${baseUrl}/api/page/${encodeURIComponent(name)}/`; + const res = await fetch(url, { signal, credentials: 'include' }); + if (!res.ok) { + throw new Error(`Failed to fetch page content: ${res.status}`); + } + return res.json(); +} diff --git a/client/tekdb-client/src/assets/react.svg b/client/src/assets/react.svg similarity index 100% rename from client/tekdb-client/src/assets/react.svg rename to client/src/assets/react.svg diff --git a/client/tekdb-client/src/index.css b/client/src/index.css similarity index 100% rename from client/tekdb-client/src/index.css rename to client/src/index.css diff --git a/client/tekdb-client/src/main.tsx b/client/src/main.tsx similarity index 100% rename from client/tekdb-client/src/main.tsx rename to client/src/main.tsx diff --git a/client/tekdb-client/src/App.tsx b/client/tekdb-client/src/App.tsx deleted file mode 100644 index 3d7ded3..0000000 --- a/client/tekdb-client/src/App.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' - -function App() { - const [count, setCount] = useState(0) - - return ( - <> - -

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ) -} - -export default App diff --git a/client/tekdb-client/vite.config.ts b/client/tekdb-client/vite.config.ts deleted file mode 100644 index 2328e17..0000000 --- a/client/tekdb-client/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react-swc' - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [react()], -}) diff --git a/client/tekdb-client/tsconfig.app.json b/client/tsconfig.app.json similarity index 100% rename from client/tekdb-client/tsconfig.app.json rename to client/tsconfig.app.json diff --git a/client/tekdb-client/tsconfig.json b/client/tsconfig.json similarity index 100% rename from client/tekdb-client/tsconfig.json rename to client/tsconfig.json diff --git a/client/tekdb-client/tsconfig.node.json b/client/tsconfig.node.json similarity index 100% rename from client/tekdb-client/tsconfig.node.json rename to client/tsconfig.node.json diff --git a/client/vite.config.ts b/client/vite.config.ts new file mode 100644 index 0000000..758c6e6 --- /dev/null +++ b/client/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react-swc' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + // forward API calls to Django dev server + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + secure: false, + }, + }, + }, +}) From a094b336b870eb0c229568858c6b9808bce6c941 Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Wed, 17 Dec 2025 15:54:57 -0800 Subject: [PATCH 03/11] fix some explore api view tests --- TEKDB/TEKDB/models.py | 2 +- TEKDB/explore/API/views.py | 31 +- TEKDB/explore/tests/test_api_views.py | 493 ++++++++++++++++++++++++++ TEKDB/explore/urls.py | 21 +- 4 files changed, 528 insertions(+), 19 deletions(-) create mode 100644 TEKDB/explore/tests/test_api_views.py diff --git a/TEKDB/TEKDB/models.py b/TEKDB/TEKDB/models.py index a4dd8d3..84040d6 100644 --- a/TEKDB/TEKDB/models.py +++ b/TEKDB/TEKDB/models.py @@ -2833,7 +2833,7 @@ def image(self): return settings.RECORD_ICONS["media"] def subtitle(self): - return self.mediatype + return str(self.mediatype) def link(self): return "/explore/media/%d/" % self.pk diff --git a/TEKDB/explore/API/views.py b/TEKDB/explore/API/views.py index 70f8848..49045bf 100644 --- a/TEKDB/explore/API/views.py +++ b/TEKDB/explore/API/views.py @@ -2,6 +2,7 @@ from rest_framework import permissions, viewsets, status from rest_framework.response import Response from rest_framework.views import APIView +# from rest_framework.generics import GenericAPIView from .serializers import PageContentSerializer from ..models import PageContent @@ -99,6 +100,22 @@ def get(self, request, name: str): } ) +class ExploreByType(APIView): + """Explore for a specific model type.""" + permission_classes = [permissions.IsAuthenticated] + + def get(self, request, model_type): + return Response( + { + "query": "", + "category": model_type, + "page": "Results", + "pageTitle": "Results", + "pageContent": "

Your search results:

", + "user": str(request.user) + } + ) + # ----- Explore/search endpoints ----- class ExploreSearch(APIView): @@ -188,11 +205,11 @@ def get(self, request): if categories == []: categories = ["all"] # sanitize categories + # Zero tolerance for mispelled or 'all' categories. if it's not perfect, fail to 'all' for category in categories: - if category not in all_categories and category != "all": + if category not in all_categories: categories = all_categories break - results = self.get_results(query_string, categories) # cap results by config @@ -226,11 +243,10 @@ def get(self, request): ) -class RecordDetail(APIView): +class ExploreById(APIView): permission_classes = [permissions.IsAuthenticated] def get(self, request, model_type, id): - models = _get_model_by_type(model_type) if len(models) != 1: return Response({"error": f"Incorrect number of models for {model_type}"}, status=status.HTTP_400_BAD_REQUEST) @@ -240,7 +256,6 @@ def get(self, request, model_type, id): record_dict = obj.get_record_dict(request.user, 3857) except Exception: raise Http404 - geo_info_needed = False if ("map" in record_dict and record_dict["map"] is not None) or any( rel.get("key") == "Place-Resource Events" and any( @@ -280,7 +295,7 @@ def get(self, request, model_type, id, format): filename = f"{model_type}_{id}_{record_dict.get('name','record')}" if format == "xls": - # Reuse original XLS export logic + # XLSX export import io from xlsxwriter.workbook import Workbook @@ -345,7 +360,7 @@ def get_sorted_keys(keys): resp["Content-Disposition"] = f'attachment; filename="{filename}.xlsx"' return resp else: - # CSV default + # CSV export import csv csv_response = HttpResponse(content_type="text/csv") csv_response["Content-Disposition"] = f'attachment; filename="{filename}.csv"' @@ -390,7 +405,7 @@ def get_sorted_keys(keys): else: writer.writerow([key, str(field)]) return csv_response - + class DownloadMediaFile(APIView): permission_classes = [permissions.IsAuthenticated] diff --git a/TEKDB/explore/tests/test_api_views.py b/TEKDB/explore/tests/test_api_views.py new file mode 100644 index 0000000..ff27ce8 --- /dev/null +++ b/TEKDB/explore/tests/test_api_views.py @@ -0,0 +1,493 @@ +from base64 import b64encode + +from django.conf import settings +from django.test import TestCase +from django.test.client import RequestFactory +from os.path import join +from unittest.mock import patch, MagicMock + +from TEKDB.tests.test_views import import_fixture_file + + +class HomeViewTest(TestCase): + def test_home_page_api_html_content(self): + with patch("explore.API.views.PageContent.objects.get") as mock_get: + mock_obj = MagicMock() + mock_obj.is_html = True + mock_obj.html_content = "Test HTML Home" + mock_get.return_value = mock_obj + response = self.client.get("/api/page/Welcome/") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["page"], "welcome") + self.assertIn("Test HTML Home", data["pageContent"]) + + def test_home_page_api_not_html_content(self): + with patch("explore.API.views.PageContent.objects.get") as mock_get: + mock_obj = MagicMock() + mock_obj.is_html = False + mock_obj.content = "Test Text Home" + mock_get.return_value = mock_obj + response = self.client.get("/api/page/Welcome/") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["pageTitle"], "Welcome") + self.assertIn("Test Text Home", data["pageContent"]) + + +class AboutViewTest(TestCase): + def test_about_page_api_html_content(self): + with patch("explore.API.views.PageContent.objects.get") as mock_get: + mock_obj = MagicMock() + mock_obj.is_html = True + mock_obj.html_content = "Test HTML About" + mock_get.return_value = mock_obj + response = self.client.get("/api/page/About/") + self.assertEqual(response.status_code, 200) + self.assertIn("Test HTML About", response.json()["pageContent"]) + + def test_about_page_api_not_html_content(self): + with patch("explore.API.views.PageContent.objects.get") as mock_get: + mock_obj = MagicMock() + mock_obj.is_html = False + mock_obj.content = "Test Text About" + mock_get.return_value = mock_obj + response = self.client.get("/api/page/About/") + self.assertEqual(response.status_code, 200) + self.assertIn("Test Text About", response.json()["pageContent"]) + + +class HelpViewTest(TestCase): + def test_help_page_api_html_content(self): + with patch("explore.API.views.PageContent.objects.get") as mock_get: + mock_obj = MagicMock() + mock_obj.is_html = True + mock_obj.html_content = "Test HTML Help" + mock_get.return_value = mock_obj + response = self.client.get("/api/page/Help/") + self.assertEqual(response.status_code, 200) + self.assertIn("Test HTML Help", response.json()["pageContent"]) + + def test_help_page_api_not_html_content(self): + with patch("explore.API.views.PageContent.objects.get") as mock_get: + mock_obj = MagicMock() + mock_obj.is_html = False + mock_obj.content = "Test Text Help" + mock_get.return_value = mock_obj + response = self.client.get("/api/page/Help/") + self.assertEqual(response.status_code, 200) + self.assertIn("Test Text Help", response.json()["pageContent"]) + + +class ExploreViewTest(TestCase): + def setUp(self): + import_fixture_file( + join(settings.BASE_DIR, "TEKDB", "fixtures", "all_dummy_data.json") + ) + + self.factory = RequestFactory() + self.credentials = b64encode(b"admin:admin").decode("ascii") + + def test_explore_api(self): + from TEKDB.models import Users + + user = Users.objects.get(username="admin") + self.client.force_login(user) + url = "/api/search/?query=test&category=places" + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["categories"], ["places"]) + +class GetByModelTypeTest(TestCase): + def setUp(self): + import_fixture_file( + join(settings.BASE_DIR, "TEKDB", "fixtures", "all_dummy_data.json") + ) + + self.factory = RequestFactory() + self.credentials = b64encode(b"admin:admin").decode("ascii") + + def test_get_by_model_type(self): + # Test that a valid model_type returns the expected records + from TEKDB.models import Users + + user = Users.objects.get(username="admin") + self.client.force_login(user) + url = "/api/explore/Places/" + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["category"], "Places") + + +class SearchTest(TestCase): + # fixtures = ['TEKDB/fixtures/all_dummy_data.json',] + + def setUp(self): + import_fixture_file( + join(settings.BASE_DIR, "TEKDB", "fixtures", "all_dummy_data.json") + ) + + self.factory = RequestFactory() + self.credentials = b64encode(b"admin:admin").decode("ascii") + + def test_multi_word_search(self): + # Test that the query string submitted matches the query string returned to the client/user + from TEKDB.models import Users + + query_string = "A multi word search" + self.client.force_login(Users.objects.get(username="admin")) + response = self.client.get(f"/api/search/?query={query_string}") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["query"], query_string) + + +class GetByModelIdTest(TestCase): + def setUp(self): + import_fixture_file( + join(settings.BASE_DIR, "TEKDB", "fixtures", "all_dummy_data.json") + ) + + self.factory = RequestFactory() + self.credentials = b64encode(b"admin:admin").decode("ascii") + + def test_get_by_model_id(self): + # Test that a valid model_type and id returns the expected record + from TEKDB.models import ( + Users, + Places, + Resources, + Media, + PlacesResourceEvents, + Citations, + ResourcesActivityEvents, + ) + + user = Users.objects.get(username="admin") + self.client.force_login(user) + + place = Places.objects.first() + url = f"/api/explore/Places/{place.pk}/" + place_response = self.client.get(url) + self.assertEqual(place_response.json()["model"].lower(), "places") + self.assertEqual(place_response.status_code, 200) + + resource = Resources.objects.first() + url = f"/api/explore/resources/{resource.pk}/" + resource_response = self.client.get(url) + self.assertEqual(resource_response.json()["model"].lower(), "resources") + self.assertEqual(resource_response.status_code, 200) + + rae = ResourcesActivityEvents.objects.first() + url = f"/api/explore/resourcesactivityevents/{rae.pk}/" + rae_response = self.client.get(url) + self.assertEqual(rae_response.json()["model"].lower(), "resourcesactivityevents") + self.assertEqual(rae_response.status_code, 200) + + media = Media.objects.first() + url = f"/api/explore/media/{media.pk}/" + media_response = self.client.get(url) + self.assertEqual(media_response.json()["model"].lower(), "media") + self.assertEqual(media_response.status_code, 200) + + pre = PlacesResourceEvents.objects.first() + url = f"/api/explore/placesresourceevents/{pre.pk}/" + pre_response = self.client.get(url) + self.assertEqual(pre_response.json()["model"].lower(), "placesresourceevents") + self.assertEqual(pre_response.status_code, 200) + + citation = Citations.objects.first() + url = f"/api/explore/citations/{citation.pk}/" + citation_response = self.client.get(url) + self.assertEqual(citation_response.json()["model"].lower(), "citations") + self.assertEqual(citation_response.status_code, 200) + + def test_get_by_model_id_invalid_model(self): + # Test that an invalid model_type returns an error message + from TEKDB.models import Users + + user = Users.objects.get(username="admin") + self.client.force_login(user) + url = "/api/explore/InvalidModel/1/" + response = self.client.get(url) + self.assertEqual(response.status_code, 400) + self.assertIn("Incorrect number of models", response.content.decode()) + + def test_get_by_model_id_invalid_id(self): + # Test that an invalid id returns an error message + from TEKDB.models import Users + + user = Users.objects.get(username="admin") + self.client.force_login(user) + url = "/api/explore/Places/999999/" + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + def test_get_by_model_id_with_map(self): + # Test that a valid model_type and id returns the expected record with map context + from TEKDB.models import Users, Places + + place = Places.objects.first() + user = Users.objects.get(username="admin") + self.client.force_login(user) + url = f"/api/explore/Places/{place.pk}/" + response = self.client.get(url) + data = response.json().get("map", {}) + map_keys = [ + "default_lon", + "default_lat", + "default_zoom", + "min_zoom", + "max_zoom", + "extent", + ] + for key in map_keys: + self.assertIn(key, data) + + +class DownloadMediaFileTest(TestCase): + def setUp(self): + import_fixture_file( + join(settings.BASE_DIR, "TEKDB", "fixtures", "all_dummy_data.json") + ) + + self.factory = RequestFactory() + self.credentials = b64encode(b"admin:admin").decode("ascii") + + def test_download_media_file(self): + from TEKDB.models import Users, Media + + user = Users.objects.get(username="admin") + self.client.force_login(user) + + media = Media.objects.first() + url = f"/api/explore/Media/{media.pk}/download" + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertEqual( + response["Content-Disposition"], + f"attachment; filename={media.mediafile}", + ) + self.assertEqual(response["Content-Type"], "application/force-download") + + def test_download_media_file_invalid_id(self): + from TEKDB.models import Users + + user = Users.objects.get(username="admin") + self.client.force_login(user) + + url = "/api/explore/Media/999999/download" + response = self.client.get(url) + + self.assertEqual(response.status_code, 404) + + def test_invalid_model_type(self): + from TEKDB.models import Users + + user = Users.objects.get(username="admin") + self.client.force_login(user) + + url = "/api/explore/InvalidModel/1/download" + response = self.client.get(url) + + self.assertEqual(response.status_code, 404) + + +class GetSortedKeysTest(TestCase): + def setUp(self): + from os.path import join + from django.conf import settings + from TEKDB.tests.test_views import import_fixture_file + + import_fixture_file( + join(settings.BASE_DIR, "TEKDB", "fixtures", "all_dummy_data.json") + ) + from TEKDB.models import Users + + self.user = Users.objects.get(username="admin") + self.client.force_login(self.user) + # curl -u admin -H 'Accept: application/json; indent=4' http://localhost:8000/api/export/Places/31/csv/ + def test_sorted_keys_via_export_csv(self): + # Indirectly verify ordering through CSV export payload + from TEKDB.models import Places + + place = Places.objects.get(pk=31) + url = f"/api/export/Places/{place.pk}/csv/" + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + content = response.content.decode() + self.assertIn("name", content) + self.assertIn("map", content) + + +class ExportRecordCsvTest(TestCase): + + def test_export_record_csv(self): + from os.path import join + from django.conf import settings + from TEKDB.tests.test_views import import_fixture_file + from TEKDB.models import Users + + import_fixture_file( + join(settings.BASE_DIR, "TEKDB", "fixtures", "all_dummy_data.json") + ) + + self.user = Users.objects.get(username="admin") + self.client.force_login(self.user) + from TEKDB.models import Places + + place = Places.objects.get(pk=31) + url = f"/api/export/Places/{place.pk}/csv/" + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response["Content-Type"], "text/csv") + + self.assertIn( + f'attachment; filename="Places_{place.pk}_{str(place)}.csv"', + response["Content-Disposition"], + ) + self.assertIn("id", response.content.decode()) + self.assertIn("name", response.content.decode()) + + +class ExportRecordXlsTest(TestCase): + def setUp(self): + from os.path import join + from django.conf import settings + from TEKDB.tests.test_views import import_fixture_file + + import_fixture_file( + join(settings.BASE_DIR, "TEKDB", "fixtures", "all_dummy_data.json") + ) + from TEKDB.models import Users + + self.user = Users.objects.get(username="admin") + self.client.force_login(self.user) + + def test_export_record_xls(self): + from TEKDB.models import Places + + place = Places.objects.get(pk=31) + url = f"/api/export/Places/{place.pk}/xls/" + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertEqual( + response["Content-Type"], + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ) + + self.assertIn( + f'attachment; filename="Places_{place.pk}_{str(place)}.xlsx"', + response["Content-Disposition"], + ) + self.assertEqual( + response.content[:2], b"PK" + ) # XLSX files start with 'PK' (zip signature) + + +class SearchViewTest(TestCase): + def setUp(self): + from os.path import join + from django.conf import settings + from TEKDB.tests.test_views import import_fixture_file + from TEKDB.models import Users + + import_fixture_file( + join(settings.BASE_DIR, "TEKDB", "fixtures", "all_dummy_data.json") + ) + self.user = Users.objects.get(username="admin") + self.client.force_login(self.user) + + def test_search_api_get_request(self): + url = "/api/search/?query=test&category=places" + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["query"], "test") + self.assertEqual(data["categories"], ["places"]) + + def test_search_api_misspelled_category(self): + url = "/api/search/?query=test&category=placess,resourcess" + response = self.client.get(url) + # should resort to all + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["query"], "test") + self.assertEqual( + data["categories"], + ["places", "resources", "activities", "sources", "media"], + ) + + def test_search_api_no_category(self): + url = "/api/search/?query=test" + response = self.client.get(url) + # should resort to all + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["query"], "test") + self.assertEqual( + data["categories"], + ["places", "resources", "activities", "sources", "media"], + ) + + def test_search_api_post_request(self): + url = "/api/search/" + data = { + "query": "test", + "activities": "on", + "citations": "on", + "media": "on", + } + response = self.client.post(url, data) + # DRF ExploreSearch currently implements GET; POST returns 405 + self.assertIn(response.status_code, [200, 405]) + + +class DownloadViewTest(TestCase): + def setUp(self): + from os.path import join + from django.conf import settings + from TEKDB.tests.test_views import import_fixture_file + + import_fixture_file( + join(settings.BASE_DIR, "TEKDB", "fixtures", "all_dummy_data.json") + ) + from TEKDB.models import Users + + self.user = Users.objects.get(username="admin") + self.client.force_login(self.user) + + def test_download_csv(self): + url = "/api/export?query=test&places=true&format=csv" + response = self.client.get(url, follow=True) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response["Content-Type"], "text/csv") + self.assertIn( + 'attachment; filename="TEK_RESULTS.csv"', response["Content-Disposition"] + ) + self.assertIn("id", response.content.decode()) + self.assertIn("name", response.content.decode()) + + def test_download_xlsx(self): + url = "/api/export?query=test&places=true&format=xlsx" + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertEqual( + response["Content-Type"], + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ) + self.assertIn( + "attachment; filename=TEK_RESULTS.xlsx", response["Content-Disposition"] + ) + self.assertEqual( + response.content[:2], b"PK" + ) # XLSX files start with 'PK' (zip signature) diff --git a/TEKDB/explore/urls.py b/TEKDB/explore/urls.py index 44bc7be..655f011 100644 --- a/TEKDB/explore/urls.py +++ b/TEKDB/explore/urls.py @@ -1,12 +1,8 @@ from django.urls import include, path, re_path -from rest_framework import routers from .API import views as api_views from . import views -router = routers.DefaultRouter() -router.register(r"pagecontent", api_views.PageContentViewSet) - explore_patterns = [ path("", views.explore), re_path(r"^(?P\w+)/$", views.get_by_model_type), @@ -14,6 +10,12 @@ re_path(r"^(?P\w+)/(?P\w+)/download$", views.download_media_file), ] +api_explore_patterns = [ + path("/", api_views.ExploreByType.as_view()), + path("//", api_views.ExploreById.as_view()), + path("//download", api_views.DownloadMediaFile.as_view()), +] + export_patterns = [ path("", views.download), re_path( @@ -29,13 +31,12 @@ path("explore/", include(explore_patterns)), path("export", views.download), path("export/", include(export_patterns)), + path("", views.home), + # API endpoints (DRF) - path("api/pagecontent/", include(router.urls)), - re_path(r"^api/page/(?P\w+)/$", api_views.PageContentSingle.as_view()), + path("api/explore/", include(api_explore_patterns)), + path("api/page//", api_views.PageContentSingle.as_view()), path("api/search/", api_views.ExploreSearch.as_view()), - re_path(r"^api/record/(?P\w+)/(?P\w+)/$", api_views.RecordDetail.as_view()), - re_path(r"^api/export/(?P\w+)/(?P\w+)/(?P\w+)/$", api_views.ExportRecord.as_view()), - re_path(r"^api/media/(?P\w+)/(?P\w+)/download$", api_views.DownloadMediaFile.as_view()), - path("", views.home), + path("api/export////", api_views.ExportRecord.as_view()), ] # url(r'^logout$', views.logout, name='logout'), From 90c48f27174e30b96489321f26443af99cb53ee0 Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Fri, 19 Dec 2025 11:39:19 -0800 Subject: [PATCH 04/11] add api for site configuration use site configuration in client --- TEKDB/TEKDB/settings.py | 2 +- TEKDB/explore/API/serializers.py | 19 +- TEKDB/explore/API/views.py | 329 +++++++++++++++++++++++--- TEKDB/explore/tests/test_api_views.py | 12 +- TEKDB/explore/urls.py | 10 +- TEKDB/explore/views.py | 1 - client/package-lock.json | 310 +++++++++++++++++++++++- client/package.json | 2 + client/src/App.css | 46 +--- client/src/App.tsx | 105 ++++---- client/src/api/pageContent.ts | 18 +- client/src/components/header.css | 117 +++++++++ client/src/components/header.tsx | 128 ++++++++++ client/src/index.css | 58 ++++- client/src/main.tsx | 1 + 15 files changed, 1016 insertions(+), 142 deletions(-) create mode 100644 client/src/components/header.css create mode 100644 client/src/components/header.tsx diff --git a/TEKDB/TEKDB/settings.py b/TEKDB/TEKDB/settings.py index d06be2c..2f0a8a7 100644 --- a/TEKDB/TEKDB/settings.py +++ b/TEKDB/TEKDB/settings.py @@ -70,7 +70,7 @@ "Relationships", "reversion", "django.contrib.sites", - "rest_framework" + "rest_framework", # 'moderation.apps.SimpleModerationConfig', ] diff --git a/TEKDB/explore/API/serializers.py b/TEKDB/explore/API/serializers.py index f2ca9de..29ab276 100644 --- a/TEKDB/explore/API/serializers.py +++ b/TEKDB/explore/API/serializers.py @@ -1,8 +1,23 @@ from ..models import PageContent from rest_framework import serializers + class PageContentSerializer(serializers.ModelSerializer): class Meta: model = PageContent - fields = ['page', 'content', 'is_html', 'html_content'] - read_only_fields = ['page'] # Make 'page' read-only to prevent changes + fields = ["page", "content", "is_html", "html_content"] + read_only_fields = ["page"] # Make 'page' read-only to prevent changes + + +class SiteConfigurationSerializer(serializers.Serializer): + proj_logo_text = serializers.CharField(max_length=100) + proj_text_placement = serializers.CharField(max_length=50) + proj_css = serializers.DictField(child=serializers.CharField(max_length=100)) + proj_icons = serializers.DictField(child=serializers.CharField(max_length=100)) + proj_image_select = serializers.CharField(max_length=100) + home_image_attribution = serializers.CharField(max_length=255, allow_blank=True) + home_font_color = serializers.CharField(max_length=7) + homepage_left_background = serializers.CharField(max_length=7) + homepage_right_background = serializers.CharField(max_length=7) + map_pin = serializers.CharField(max_length=100) + map_pin_selected = serializers.CharField(max_length=100) diff --git a/TEKDB/explore/API/views.py b/TEKDB/explore/API/views.py index 49045bf..d09a4e0 100644 --- a/TEKDB/explore/API/views.py +++ b/TEKDB/explore/API/views.py @@ -4,7 +4,7 @@ from rest_framework.views import APIView # from rest_framework.generics import GenericAPIView -from .serializers import PageContentSerializer +from .serializers import PageContentSerializer, SiteConfigurationSerializer from ..models import PageContent @@ -12,14 +12,29 @@ class PageContentViewSet(viewsets.ModelViewSet): """ CRUD for PageContent via DRF. """ + serializer_class = PageContentSerializer queryset = PageContent.objects.all() - permission_classes = [permissions.IsAuthenticatedOrReadOnly] + permission_classes = [permissions.AllowAny] + + +class SiteConfigurationAPIView(APIView): + permission_classes = [permissions.AllowAny] + + def get(self, request): + from ..context_processors import explore_context + + # Get configuration data from context processor + config_data = explore_context(request) + + serializer = SiteConfigurationSerializer(config_data) + return Response(serializer.data) # ----- Utility functions ported from template views ----- def _get_project_geography(): from TEKDB.settings import DATABASE_GEOGRAPHY + return DATABASE_GEOGRAPHY @@ -81,13 +96,16 @@ def _get_model_by_type(model_type): # ----- Page content endpoints ----- class PageContentSingle(APIView): """Return single page content by name (Home/About/Help).""" + permission_classes = [permissions.AllowAny] def get(self, request, name: str): try: page_content_obj = PageContent.objects.get(page=name) page_content = ( - page_content_obj.html_content if page_content_obj.is_html else page_content_obj.content + page_content_obj.html_content + if page_content_obj.is_html + else page_content_obj.content ) except Exception: page_content = f"

{name}

Set {name} Page Content In Admin

" @@ -100,8 +118,10 @@ def get(self, request, name: str): } ) + class ExploreByType(APIView): """Explore for a specific model type.""" + permission_classes = [permissions.IsAuthenticated] def get(self, request, model_type): @@ -112,7 +132,7 @@ def get(self, request, model_type): "page": "Results", "pageTitle": "Results", "pageContent": "

Your search results:

", - "user": str(request.user) + "user": str(request.user), } ) @@ -129,7 +149,11 @@ def get_verbose_field_name(self, model, field_path): field = current_model._meta.get_field(part) if hasattr(field, "related_model") and field.related_model: current_model = field.related_model - return field.verbose_name.title() if field else field_path.replace("_", " ").title() + return ( + field.verbose_name.title() + if field + else field_path.replace("_", " ").title() + ) def remove_match_prefix(self, string): if string and string.startswith("match_"): @@ -171,12 +195,24 @@ def get_results(self, keyword_string, categories): pks[result.pk] = pks.get(result.pk, 0) + 1 for result in model_results: greatest_attr = self.get_greatest_similarity_attribute(result, pks) - actual_attribute = self.remove_match_prefix(greatest_attr) if greatest_attr else None + actual_attribute = ( + self.remove_match_prefix(greatest_attr) + if greatest_attr + else None + ) verbose_name = ( - self.get_verbose_field_name(model, actual_attribute) if actual_attribute else None + self.get_verbose_field_name(model, actual_attribute) + if actual_attribute + else None + ) + headline_key = ( + f"headline_{actual_attribute}" if actual_attribute else None + ) + headline_value = ( + getattr(result, headline_key) + if headline_key and hasattr(result, headline_key) + else None ) - headline_key = f"headline_{actual_attribute}" if actual_attribute else None - headline_value = getattr(result, headline_key) if headline_key and hasattr(result, headline_key) else None result_json = result.get_response_format() if keyword_string != "": result_json["rank"] = result.rank @@ -191,12 +227,18 @@ def get_results(self, keyword_string, categories): result_json["similarity"] = 0 result_json["headline"] = None resultlist.append(result_json) - return sorted(resultlist, key=lambda res: (res["rank"], res["similarity"]), reverse=True) + return sorted( + resultlist, key=lambda res: (res["rank"], res["similarity"]), reverse=True + ) def get(self, request): all_categories = ["places", "resources", "activities", "sources", "media"] query_string = request.GET.get("query") - categories = request.GET.getlist("category") or [request.GET.get("category")] if request.GET.get("category") else [] + categories = ( + request.GET.getlist("category") or [request.GET.get("category")] + if request.GET.get("category") + else [] + ) if categories == []: # derive from flags or default to all for key in all_categories: @@ -215,9 +257,11 @@ def get(self, request): # cap results by config try: from configuration.models import Configuration + max_results = Configuration.objects.all()[0].max_results_returned except Exception: from TEKDB.settings import DEFAULT_MAXIMUM_RESULTS + max_results = DEFAULT_MAXIMUM_RESULTS too_many = len(results) > max_results @@ -249,7 +293,10 @@ class ExploreById(APIView): def get(self, request, model_type, id): models = _get_model_by_type(model_type) if len(models) != 1: - return Response({"error": f"Incorrect number of models for {model_type}"}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {"error": f"Incorrect number of models for {model_type}"}, + status=status.HTTP_400_BAD_REQUEST, + ) model = models[0] try: obj = model.objects.get(pk=int(id)) @@ -258,9 +305,8 @@ def get(self, request, model_type, id): raise Http404 geo_info_needed = False if ("map" in record_dict and record_dict["map"] is not None) or any( - rel.get("key") == "Place-Resource Events" and any( - (val.get("map") is not None) for val in rel.get("value", []) - ) + rel.get("key") == "Place-Resource Events" + and any((val.get("map") is not None) for val in rel.get("value", [])) for rel in record_dict.get("relationships", []) ): geo_info_needed = True @@ -285,15 +331,21 @@ class ExportRecord(APIView): def get(self, request, model_type, id, format): models = _get_model_by_type(model_type) if len(models) != 1: - return Response({"error": f"Incorrect number of models for {model_type}"}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {"error": f"Incorrect number of models for {model_type}"}, + status=status.HTTP_400_BAD_REQUEST, + ) model = models[0] try: obj = model.objects.get(pk=id) record_dict = obj.get_record_dict(request.user, 4326) except Exception as e: - return Response({"error": "unknown error", "code": f"{e}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response( + {"error": "unknown error", "code": f"{e}"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) - filename = f"{model_type}_{id}_{record_dict.get('name','record')}" + filename = f"{model_type}_{id}_{record_dict.get('name', 'record')}" if format == "xls": # XLSX export import io @@ -304,6 +356,7 @@ def get(self, request, model_type, id, format): worksheet = workbook.add_worksheet() workbook.add_format({"bold": True}) row = 0 + # helper for ordered keys def get_sorted_keys(keys): ordered = [] @@ -327,13 +380,22 @@ def get_sorted_keys(keys): for key in get_sorted_keys(list(record_dict.keys())): field = record_dict[key] - if isinstance(field, list) and len(field) > 0 and isinstance(field[0], dict): + if ( + isinstance(field, list) + and len(field) > 0 + and isinstance(field[0], dict) + ): for item in field: - if ("key" in item and "value" in item and len(item.keys()) == 2): - if isinstance(item["value"], list) and len(item["value"]) > 0: + if "key" in item and "value" in item and len(item.keys()) == 2: + if ( + isinstance(item["value"], list) + and len(item["value"]) > 0 + ): for sub_item in item["value"]: worksheet.write(row, 0, f"{key} - {item['key']}") - worksheet.write(row, 1, sub_item.get("name", str(sub_item))) + worksheet.write( + row, 1, sub_item.get("name", str(sub_item)) + ) row += 1 else: worksheet.write(row, 0, f"{key} - {item['key']}") @@ -362,8 +424,11 @@ def get_sorted_keys(keys): else: # CSV export import csv + csv_response = HttpResponse(content_type="text/csv") - csv_response["Content-Disposition"] = f'attachment; filename="{filename}.csv"' + csv_response["Content-Disposition"] = ( + f'attachment; filename="{filename}.csv"' + ) writer = csv.writer(csv_response) def get_sorted_keys(keys): @@ -388,24 +453,40 @@ def get_sorted_keys(keys): for key in get_sorted_keys(list(record_dict.keys())): field = record_dict[key] - if isinstance(field, list) and len(field) > 0 and isinstance(field[0], dict): + if ( + isinstance(field, list) + and len(field) > 0 + and isinstance(field[0], dict) + ): for item in field: - if ("key" in item and "value" in item and len(item.keys()) == 2): - if isinstance(item["value"], list) and len(item["value"]) > 0: + if "key" in item and "value" in item and len(item.keys()) == 2: + if ( + isinstance(item["value"], list) + and len(item["value"]) > 0 + ): for sub_item in item["value"]: - if isinstance(sub_item, dict) and "name" in sub_item: - writer.writerow([f"{key} - {item['key']}", sub_item["name"]]) + if ( + isinstance(sub_item, dict) + and "name" in sub_item + ): + writer.writerow( + [f"{key} - {item['key']}", sub_item["name"]] + ) else: - writer.writerow([f"{key} - {item['key']}", str(sub_item)]) + writer.writerow( + [f"{key} - {item['key']}", str(sub_item)] + ) else: - writer.writerow([f"{key} - {item['key']}", item["value"]]) + writer.writerow( + [f"{key} - {item['key']}", item["value"]] + ) else: for list_key in item.keys(): writer.writerow([f"{key} - {list_key}", item[list_key]]) else: writer.writerow([key, str(field)]) return csv_response - + class DownloadMediaFile(APIView): permission_classes = [permissions.IsAuthenticated] @@ -435,3 +516,189 @@ def get(self, request, model_type, id): return response raise Http404 + +# ----- Export endpoints ----- +class Download(APIView): + permission_classes = [permissions.IsAuthenticated] + + def get_category_list(self, request): + categories = [] + for category in ["places", "resources", "activities", "sources", "media"]: + if request.GET.get(category) == "true": + categories.append(category) + return categories + + def find_match_attributes(self, obj): + match_attributes = [attr for attr in dir(obj) if "match" in attr] + return match_attributes + + def get_greatest_similarity_attribute(self, result, pks): + greatest_similarity_attribute = None + matching_attributes = [] + + # get headline and similarity results + for match_attr in self.find_match_attributes(result): + match_value = getattr(result, match_attr) + if match_value is not None: + if match_value == result.similarity: + matching_attributes.append(match_attr) + + # If multiple attributes have the same similarity, choose based on number of matching IDs + if len(matching_attributes) == 1: + greatest_similarity_attribute = matching_attributes[0] + elif len(matching_attributes) > 0: + num_same_id = pks[result.pk] + if num_same_id - 1 < len(matching_attributes): + greatest_similarity_attribute = matching_attributes[num_same_id - 1] + pks[result.pk] -= 1 + else: + greatest_similarity_attribute = matching_attributes[0] + else: + greatest_similarity_attribute = None + + return greatest_similarity_attribute + + def remove_match_prefix(self, string): + if string.startswith("match_"): + return string[6:] + return string + + def get_verbose_field_name(self, model, field_path): + parts = field_path.split("__") + field = None + current_model = model + + for part in parts: + field = current_model._meta.get_field(part) + + # If it's a related field, follow to the related model + if hasattr(field, "related_model") and field.related_model: + current_model = field.related_model + + return ( + field.verbose_name.title() + if field + else field_path.replace("_", " ").title() + ) + + def get_results(self, keyword_string, categories): + if keyword_string is None: + keyword_string = "" + + resultlist = [] + + for category in categories: + query_models = _get_model_by_type(category) + for model in query_models: + # Find all results matching keyword in this model + model_results = model.keyword_search(keyword_string) + pks = {} + # Count number of times each pk appears in results + for result in model_results: + if hasattr(result, "pk"): + if result.pk not in pks: + pks[result.pk] = 1 + else: + pks[result.pk] += 1 + + for result in model_results: + actual_attribute = None + headline_value = None + + greatest_similarity_attribute = ( + self.get_greatest_similarity_attribute(result, pks) + ) + + actual_attribute = ( + self.remove_match_prefix(greatest_similarity_attribute) + if greatest_similarity_attribute + else None + ) + verbose_name = ( + self.get_verbose_field_name(model, actual_attribute) + if actual_attribute + else None + ) + + headline_key = f"headline_{actual_attribute}" + if hasattr(result, headline_key): + headline_value = getattr(result, headline_key) + + # Create JSON object to be returned + result_json = result.get_response_format() + if keyword_string != "": + result_json["rank"] = result.rank + result_json["similarity"] = result.similarity + result_json["headline"] = ( + f"

{verbose_name}: {headline_value}

" + if headline_value and verbose_name + else None + ) + else: + result_json["rank"] = 0 + result_json["similarity"] = 0 + result_json["headline"] = None + + resultlist.append(result_json) + # Sort results from all models by rank, then similarity (descending) + return sorted( + resultlist, key=lambda res: (res["rank"], res["similarity"]), reverse=True + ) + + def get(self, request): + categories = self.get_category_list(request) + results = self.get_results(request.GET.get("query"), categories) + format_type = request.GET.get("format") + filename = "TEK_RESULTS" + fieldnames = ["id", "name", "description", "type"] + rows = [] + for row in results: + row_dict = {} + for field in fieldnames: + row_dict[field] = row[field] if row[field] else " " + rows.append(row_dict) + + if format_type == "xlsx": + import io + from xlsxwriter.workbook import Workbook + + output = io.BytesIO() + workbook = Workbook(output, {"in_membory": True}) + worksheet = workbook.add_worksheet() + bold = workbook.add_format({"bold": True}) + rows.insert(0, fieldnames) + row = 0 + col = 0 + for entry in rows: + for field in fieldnames: + if row == 0: + worksheet.write(0, col, field, bold) + else: + worksheet.write(row, col, entry[field]) + col += 1 + row += 1 + col = 0 + workbook.close() + output.seek(0) + xls_response = HttpResponse( + output.read(), + content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ) + xls_response["Content-Disposition"] = ( + "attachment; filename=%s.xlsx" % filename + ) + return xls_response + + else: + # if format_type == 'csv': + import csv + + csv_response = HttpResponse(content_type="text/csv") + csv_response["Content-Disposition"] = ( + 'attachment; filename="%s.csv"' % filename + ) + writer = csv.DictWriter(csv_response, fieldnames=fieldnames) + writer.writeheader() + for row in rows: + writer.writerow(row) + return csv_response diff --git a/TEKDB/explore/tests/test_api_views.py b/TEKDB/explore/tests/test_api_views.py index ff27ce8..df8f36c 100644 --- a/TEKDB/explore/tests/test_api_views.py +++ b/TEKDB/explore/tests/test_api_views.py @@ -100,6 +100,7 @@ def test_explore_api(self): data = response.json() self.assertEqual(data["categories"], ["places"]) + class GetByModelTypeTest(TestCase): def setUp(self): import_fixture_file( @@ -183,7 +184,9 @@ def test_get_by_model_id(self): rae = ResourcesActivityEvents.objects.first() url = f"/api/explore/resourcesactivityevents/{rae.pk}/" rae_response = self.client.get(url) - self.assertEqual(rae_response.json()["model"].lower(), "resourcesactivityevents") + self.assertEqual( + rae_response.json()["model"].lower(), "resourcesactivityevents" + ) self.assertEqual(rae_response.status_code, 200) media = Media.objects.first() @@ -309,6 +312,7 @@ def setUp(self): self.user = Users.objects.get(username="admin") self.client.force_login(self.user) + # curl -u admin -H 'Accept: application/json; indent=4' http://localhost:8000/api/export/Places/31/csv/ def test_sorted_keys_via_export_csv(self): # Indirectly verify ordering through CSV export payload @@ -317,7 +321,7 @@ def test_sorted_keys_via_export_csv(self): place = Places.objects.get(pk=31) url = f"/api/export/Places/{place.pk}/csv/" response = self.client.get(url) - + self.assertEqual(response.status_code, 200) content = response.content.decode() self.assertIn("name", content) @@ -325,7 +329,6 @@ def test_sorted_keys_via_export_csv(self): class ExportRecordCsvTest(TestCase): - def test_export_record_csv(self): from os.path import join from django.conf import settings @@ -478,7 +481,8 @@ def test_download_csv(self): def test_download_xlsx(self): url = "/api/export?query=test&places=true&format=xlsx" - response = self.client.get(url) + url = "/api/export?query=test&places=true&format=csv" + response = self.client.get(url, follow=True) self.assertEqual(response.status_code, 200) self.assertEqual( diff --git a/TEKDB/explore/urls.py b/TEKDB/explore/urls.py index 655f011..c94d0e4 100644 --- a/TEKDB/explore/urls.py +++ b/TEKDB/explore/urls.py @@ -32,11 +32,17 @@ path("export", views.download), path("export/", include(export_patterns)), path("", views.home), - # API endpoints (DRF) path("api/explore/", include(api_explore_patterns)), path("api/page//", api_views.PageContentSingle.as_view()), path("api/search/", api_views.ExploreSearch.as_view()), - path("api/export////", api_views.ExportRecord.as_view()), + path("api/export/", api_views.Download.as_view()), + path( + "api/export////", + api_views.ExportRecord.as_view(), + ), + path( + "api/site-info/", api_views.SiteConfigurationAPIView.as_view(), name="site-info" + ), ] # url(r'^logout$', views.logout, name='logout'), diff --git a/TEKDB/explore/views.py b/TEKDB/explore/views.py index 313746f..24c547f 100644 --- a/TEKDB/explore/views.py +++ b/TEKDB/explore/views.py @@ -740,4 +740,3 @@ def download(request): for row in rows: writer.writerow(row) return csv_response - diff --git a/client/package-lock.json b/client/package-lock.json index d0ab146..9b51f2f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,7 +8,9 @@ "name": "tekdb-client", "version": "0.0.0", "dependencies": { + "bootstrap": "^5.3.8", "react": "^19.2.0", + "react-bootstrap": "^2.10.10", "react-dom": "^19.2.0" }, "devDependencies": { @@ -218,6 +220,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -967,6 +978,85 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@restart/hooks": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", + "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.9.4.tgz", + "integrity": "sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@popperjs/core": "^2.11.8", + "@react-aria/ssr": "^3.5.0", + "@restart/hooks": "^0.5.0", + "@types/warning": "^3.0.3", + "dequal": "^2.0.3", + "dom-helpers": "^5.2.0", + "uncontrollable": "^8.0.4", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/@restart/ui/node_modules/@restart/hooks": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.5.1.tgz", + "integrity": "sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui/node_modules/uncontrollable": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz", + "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.14.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.47", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", @@ -1498,6 +1588,15 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@swc/types": { "version": "0.1.25", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", @@ -1532,11 +1631,16 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1552,6 +1656,21 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/warning": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", + "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.49.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz", @@ -1918,6 +2037,25 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bootstrap": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", + "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2011,6 +2149,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2064,7 +2208,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -2092,6 +2235,25 @@ "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.267", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", @@ -2553,6 +2715,15 @@ "node": ">=0.8.19" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2587,7 +2758,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -2697,6 +2867,18 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2760,6 +2942,15 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2902,6 +3093,30 @@ "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "license": "MIT", + "dependencies": { + "react-is": "^16.3.2", + "warning": "^4.0.0" + }, + "peerDependencies": { + "react": ">=0.14.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2921,6 +3136,37 @@ "node": ">=0.10.0" } }, + "node_modules/react-bootstrap": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.10.tgz", + "integrity": "sha512-gMckKUqn8aK/vCnfwoBpBVFUGT9SVQxwsYrp9yDHt0arXMamxALerliKBxr1TPbntirK/HGrUAHYbAeQTa9GHQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@restart/hooks": "^0.4.9", + "@restart/ui": "^1.9.4", + "@types/prop-types": "^15.7.12", + "@types/react-transition-group": "^4.4.6", + "classnames": "^2.3.2", + "dom-helpers": "^5.2.1", + "invariant": "^2.2.4", + "prop-types": "^15.8.1", + "prop-types-extra": "^1.1.0", + "react-transition-group": "^4.4.5", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "@types/react": ">=16.14.8", + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-dom": { "version": "19.2.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.2.tgz", @@ -2933,6 +3179,34 @@ "react": "^19.2.2" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3090,6 +3364,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3141,6 +3421,21 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -3264,6 +3559,15 @@ } } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/client/package.json b/client/package.json index 9e12d17..214e4b5 100644 --- a/client/package.json +++ b/client/package.json @@ -10,7 +10,9 @@ "preview": "vite preview" }, "dependencies": { + "bootstrap": "^5.3.8", "react": "^19.2.0", + "react-bootstrap": "^2.10.10", "react-dom": "^19.2.0" }, "devDependencies": { diff --git a/client/src/App.css b/client/src/App.css index b9d355d..0573a85 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -1,42 +1,6 @@ #root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} + display: flex; + flex-direction: column; + min-height: 100vh; + width: 100%; +} \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index 438e3e2..f808d71 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,67 +1,84 @@ -import { useEffect, useMemo, useState } from 'react' -import './App.css' -import { fetchPageContent, type PageContentResponse } from './api/pageContent' +import { use, useEffect, useMemo, useState } from "react"; +import "./App.css"; +import { + fetchPageContent, + fetchSiteInfo, + type PageContentResponse, +} from "./api/pageContent"; +import Header from "./components/header"; +import "bootstrap/dist/css/bootstrap.min.css"; function App() { - const [pageName, setPageName] = useState<'Welcome' | 'About' | 'Help'>('Welcome') - const [data, setData] = useState(null) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) + const [pageName, setPageName] = useState<"Welcome" | "About" | "Help">( + "Welcome", + ); + const [data, setData] = useState(null); + const [siteInfo, setSiteInfo] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); - const controller = useMemo(() => new AbortController(), [pageName]) + const setCSSVariables = (theme: Record) => { + for (const value in theme) { + document.documentElement.style.setProperty(`--${value}`, theme[value]); + } + }; useEffect(() => { - setLoading(true) - setError(null) - setData(null) + const controller = new AbortController(); + + setLoading(true); + setError(null); + setData(null); fetchPageContent(pageName, controller.signal) .then((resp) => setData(resp)) .catch((e) => { - if (e.name !== 'AbortError') setError(String(e)) + if (e.name !== "AbortError") setError(String(e)); + }) + .finally(() => setLoading(false)); + return () => controller.abort(); + }, [pageName]); + + useEffect(() => { + const controller = new AbortController(); + console.log("Fetching Site Info"); + fetchSiteInfo(controller.signal) + .then((resp) => { + setSiteInfo(resp); + if (resp.proj_css) { + setCSSVariables(resp.proj_css); + } }) - .finally(() => setLoading(false)) - return () => controller.abort() - }, [pageName]) + .catch((e) => { + if (e.name !== "AbortError") { + console.error("Site info error:", e); + setError(String(e)); + } + }); + return () => controller.abort(); + }, []); return ( <> -
-

TEKDB

- -
- -
+
+
{loading &&

Loading…

} - {error &&

Error: {error}

} + {error &&

Error: {error}

} {!loading && !error && data && (
-

{data.pageTitle}

{/* pageContent may include HTML from admin; rendering as HTML intentionally */}
)}
- ) + ); } -export default App +export default App; diff --git a/client/src/api/pageContent.ts b/client/src/api/pageContent.ts index 9ae076f..8b5c400 100644 --- a/client/src/api/pageContent.ts +++ b/client/src/api/pageContent.ts @@ -4,13 +4,25 @@ export type PageContentResponse = { pageContent: string; // May contain HTML }; -const baseUrl = (import.meta.env.VITE_API_BASE_URL as string) || ''; +const baseUrl = (import.meta.env.VITE_API_BASE_URL as string) || ""; -export async function fetchPageContent(name: string, signal?: AbortSignal): Promise { +export async function fetchPageContent( + name: string, + signal?: AbortSignal +): Promise { const url = `${baseUrl}/api/page/${encodeURIComponent(name)}/`; - const res = await fetch(url, { signal, credentials: 'include' }); + const res = await fetch(url, { signal, credentials: "include" }); if (!res.ok) { throw new Error(`Failed to fetch page content: ${res.status}`); } return res.json(); } + +export async function fetchSiteInfo(signal?: AbortSignal): Promise { + const url = `${baseUrl}/api/site-info/`; + const res = await fetch(url, { signal, credentials: "include" }); + if (!res.ok) { + throw new Error(`Failed to fetch site info: ${res.status}`); + } + return await res.json(); +} diff --git a/client/src/components/header.css b/client/src/components/header.css new file mode 100644 index 0000000..1ec1ee2 --- /dev/null +++ b/client/src/components/header.css @@ -0,0 +1,117 @@ +nav.navbar { + align-items: stretch; + background-color: var(--default_background); + border-bottom: 1px solid var(--black); + height: var(--header_height); +} + +.navbar-brand { + margin-right: auto; +} + +/* Wrapper for logo */ +/* Logo value is a django variable in navbar.html */ +/* Logo is assigned as the background-image for .navbar-brand-wrapper */ +.navbar-brand-wrapper { + align-content: center; + align-items: center; + background-repeat: no-repeat; + background-size: contain; + background-position: center; + display: flex; + font-family: var(--font-sans-serif-extrabold); + font-size: 1.06375em; + letter-spacing: 0.025em; + flex-wrap: wrap; + height: 79px; + line-height: normal; + padding: 0; + text-align: center; + min-width: 120px; +} + +/* + * Customizable Logo and text position + * + * The text position can be set to left, right, or center. + * The background image will be positioned accordingly. + * For default styles see .navbar-brand-wrapper +*/ + +.navbar-brand-wrapper.text-before { + background-position: right center; + text-align: left; +} + +.navbar-brand-wrapper.text-after { + background-position: left center; + text-align: right; +} + +.navbar-brand-text { + width: 100%; +} + +/* + * Navbar Toggler + * +*/ + +ul.navbar-nav { + width: 100%; + margin-left: 10%; +} + +.navbar-toggler { + margin-bottom: calc(var(--header_height)/4); +} + +ul.navbar-nav li.nav-item { + flex-grow: 3; + text-align: center; +} + +ul.navbar-nav li.nav-item a.nav-link { + border-bottom: 3px solid transparent; + color: var(--black); + display: inline-block; + font-size: 1.125em; + font-family: var(--font-sans-serif-bold); + letter-spacing: 0.125em; + transition: border-bottom 0.2s ease-in-out; +} + +ul.navbar-nav li.nav-item a.nav-link.disabled { + color: rgba(0, 0, 0, .3); +} + +ul.navbar-nav li.nav-item a.nav-link:hover { + border-bottom: 3px solid var(--highlight_underline); + color: var(--black); +} + +ul.navbar-nav li.nav-item.active a.nav-link { + border-bottom: 3px solid var(--highlight_underline); +} + +ul.navbar-nav li.nav-item.user-menu { + text-align: right; +} + +ul.navbar-nav li.nav-item.user-menu a.header-button, +ul.navbar-nav li.nav-item.user-menu a#userDropdown { + /* background-color: var(--alt_bg); */ + font-family: var(--font-sans-serif); + font-size: .9375em; + padding: .6375em 1.375em; +} + +/* ul.navbar-nav li.nav-item.user-menu a.header-button, +ul.navbar-nav li.nav-item.user-menu a#userDropdown:hover { + border: 1px solid var(--default_border); +} */ + +ul.navbar-nav li.nav-item.user-menu ul.dropdown-menu[data-bs-popper] { + left: unset; + right: 2rem; +} \ No newline at end of file diff --git a/client/src/components/header.tsx b/client/src/components/header.tsx new file mode 100644 index 0000000..639da24 --- /dev/null +++ b/client/src/components/header.tsx @@ -0,0 +1,128 @@ +import React from "react"; +import "./header.css"; + +interface HeaderProps { + projIcons: { + logo: string; + }; + projTextPlacement: string; + projLogoText: string; + pageTitle: string; + setPageName: (name: "Welcome" | "About" | "Help") => void; + isAuthenticated?: boolean; +} + +const Header: React.FC = ({ + projIcons, + projTextPlacement, + projLogoText, + pageTitle, + setPageName, + isAuthenticated, +}) => { + // TODO: fix hardcoded URL + const logoUrl = projIcons.logo + ? `http://127.0.0.1:8000${projIcons.logo}` + : ""; + + return ( +
+ +
+ ); +}; + +export default Header; diff --git a/client/src/index.css b/client/src/index.css index 08a3ac9..aa7f27c 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,16 +1,54 @@ :root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; + --header_height: 6rem; + --black: #000000; + --white: #ffffff; + /* PRIMARY A: Vivid, unique. ORIGINAL: SIENNA */ + --current_pg_index: var(--primary_a); + --highlight_underline: var(--primary_a); + --highlight_font: var(--primary_a); + /* PRIMARY B VERY faint. ORIGINAL: OFF-WHITE */ + --light_font: var(--primary_b); + --accent_bg: var(--primary_b); + /* PRIMARY C VERY dark, bold. ORIGINAL: OFF-BLACK */ + --dark_font: var(--primary_c); + /* PRIMARY D neutral. ORIGINAL: GRAY */ + --result_highlight: var(--primary_d); + --alt_bg: var(--primary_d); + --light_border: var(--primary_d); + /* SECONDARY A Themed, dark. ORIGINAL: FOREST-GREEN */ + --link_color: var(--secondary_a); + --alt_field_bg: var(--secondary_a); + /* SECONDARY B Themed, moderate. ORIGINAL: OLIVE-GREEN */ + /* SECONDARY C Themed, vivid. ORIGINAL: GREEN */ + --ok_button_bg: var(--secondary_c); + --placeholder_font: var(--secondary_c); + /* SECONDARY D Vivid, unique, accent. ORIGINAL: FUCHSIA*/ + /* PALETTE OVERRIDES */ + --default_background: var(--white); + --default_alt_font: var(--white); + --default_font: var(--black); + --default_border: var(--black); + --btn_border: 2px solid var(--white); + --btn_color: var(--white); + /* Theme Colors */ + --theme_bg_color: var(--black); + --theme_color: var(--white); + /* FONT OVERRIDES */ + --bs-font-sans-serif: "Open Sans Regular", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-sans-serif: var(--bs-font-sans-serif); + --font-sans-serif-bold: "Open Sans Bold", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-sans-serif-extrabold: "Open Sans ExtraBold", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --small_font_size: 0.7rem; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; + /* Spacing */ + --margin_bottom: 1.5em; + --margin_top: 1.5em; + --margin: 1.5em; + --letter_spacing: 0.05em; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + /* Borders */ + --default_border: 1px solid var(--default_font); + --default_border_radius: 0.3rem; } a { diff --git a/client/src/main.tsx b/client/src/main.tsx index bef5202..2f92060 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' + createRoot(document.getElementById('root')!).render( From 8b4ecc3da2b076a0b8b2c6f06c648d72a6cc1df1 Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Fri, 19 Dec 2025 11:40:57 -0800 Subject: [PATCH 05/11] remove console.log --- client/src/App.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index f808d71..7682ffa 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -40,7 +40,6 @@ function App() { useEffect(() => { const controller = new AbortController(); - console.log("Fetching Site Info"); fetchSiteInfo(controller.signal) .then((resp) => { setSiteInfo(resp); From a5e5a12e91878dff53a82ea4b398c4528c3fb6ce Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Fri, 19 Dec 2025 13:26:07 -0800 Subject: [PATCH 06/11] add styles for main section --- client/src/App.tsx | 106 +++++++++++++++--- client/src/index.css | 261 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 350 insertions(+), 17 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 7682ffa..886e8b1 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,4 +1,4 @@ -import { use, useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import "./App.css"; import { fetchPageContent, @@ -12,15 +12,17 @@ function App() { const [pageName, setPageName] = useState<"Welcome" | "About" | "Help">( "Welcome", ); + const [isAuthenticated, setIsAuthenticated] = useState(false); const [data, setData] = useState(null); const [siteInfo, setSiteInfo] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const setCSSVariables = (theme: Record) => { - for (const value in theme) { - document.documentElement.style.setProperty(`--${value}`, theme[value]); - } + if (!theme) return; + Object.entries(theme).forEach(([key, value]) => { + document.documentElement.style.setProperty(`--${key}`, value); + }); }; useEffect(() => { @@ -32,27 +34,34 @@ function App() { fetchPageContent(pageName, controller.signal) .then((resp) => setData(resp)) .catch((e) => { - if (e.name !== "AbortError") setError(String(e)); + if (e.name !== "AbortError") { + console.error("Error fetching page content:", e); + setError("Failed to load page content."); + } }) .finally(() => setLoading(false)); + return () => controller.abort(); }, [pageName]); useEffect(() => { const controller = new AbortController(); + fetchSiteInfo(controller.signal) .then((resp) => { setSiteInfo(resp); if (resp.proj_css) { setCSSVariables(resp.proj_css); + setCSSVariables({["home_font_color"]: resp.home_font_color, ["homepage_right_background"]: resp.homepage_right_background, ["homepage_left_background"]: resp.homepage_left_background}) } }) .catch((e) => { if (e.name !== "AbortError") { - console.error("Site info error:", e); - setError(String(e)); + console.error("Error fetching site info:", e); + setError("Failed to load site information."); } }); + return () => controller.abort(); }, []); @@ -64,17 +73,80 @@ function App() { projLogoText={siteInfo?.proj_logo_text || ""} pageTitle={pageName} setPageName={setPageName} - isAuthenticated={false} + isAuthenticated={isAuthenticated} /> -
- {loading &&

Loading…

} - {error &&

Error: {error}

} - {!loading && !error && data && ( -
- {/* pageContent may include HTML from admin; rendering as HTML intentionally */} -
-
- )} +
+
+
+
+ {loading &&

Loading…

} + {error &&

Error: {error}

} + {!loading && !error && data && ( +
+
+
+
+
+ {!isAuthenticated ? ( + + ) : ( + + Explore + + )} +
+
+
+
+ project image +
+ {siteInfo && siteInfo.home_image_attribution && ( +
+ +
+

{siteInfo.home_image_attribution}

+
+
+ )} +
+
+
+ )} +
+
+
); diff --git a/client/src/index.css b/client/src/index.css index aa7f27c..ec91b52 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -104,3 +104,264 @@ button:focus-visible { background-color: #f9f9f9; } } +main { + background: var(--white); +} + +.min-full-height { + min-height: calc(100vh - 96px); +} + +h1 { + display: block; + /* margin: var(--margin_top) 0 calc(var(--margin_bottom) / 2); */ + margin-bottom: 0; + position: relative; + text-align: center; +} + +h2 { + font-size: 2em; + margin-top: var(--margin_top); +} + +p { + font-size: 1.25em; + letter-spacing: .05em; + margin: calc(var(--margin_top) / 2) 0 calc(var(--margin_bottom) * 2) +} + +.explore-form-row { + align-items: stretch; + flex-direction: row; +} + +.explore-form-row > div { + /* width: 100%; */ +} + +.explore-form { + margin-top: calc(var(--margin_top) * 2); +} + +.explore-input-group { + /* margin: var(--margin_top) 0 calc(var(--margin_bottom) * 2); */ + padding: 0 10%; +} + +hr { + font-size: 1.25em; + margin: calc(var(--margin_top) * 2) 0 calc(var(--margin_bottom) * 2); +} + +.explore-form .btn { + /* border: 1px solid var(--primary_d): */ + /* position: relative; */ +} + +.button-wrapper { + /* justify-content: ; */ + margin: calc(var(--margin_top) * 2) 0; + position: relative; + text-align: center; +} + +button.btn { + background: var(--white); + border-color: var(--black); + border-radius: 0; + color: var(--black); + font-family: var(--font-sans-serif); + font-size: 1.0375em; +} + +button.btn:hover { + background: var(--primary_a); + border-color: var(--black); + color: var(--white); +} + +.arrow-right { + display: inline-block; + position: relative; +} + +button.btn .arrow-right, +button.btn .arrow-right::before, +button.btn .arrow-right::after { + background: var(--primary_a); +} + +button.btn:hover .arrow-right, +button.btn:hover .arrow-right::before, +button.btn:hover .arrow-right::after { + background: var(--white); +} + +.form-wrapper { + margin-top: calc(var(--margin_top) * 2); + text-align: center; +} + + +.form-wrapper .explore-form h1:nth-of-type(2)::after { + background: var(--secondary_b); + border-radius: 50%; + color: var(--white); + content: "OR"; + font-size: .3125em; + height: 4em; + left: 50%; + line-height: 4em; + position: absolute; + text-align: center; + top: -5em; + transform: translateX(-50%); + width: 4em; + white-space: nowrap; +} + +#filter-checkboxes { + flex-direction: row; + padding-left: 40%; + text-transform: uppercase; + text-align: left; +} + +@media (min-width: 64em) { + #filter-checkboxes { + padding-left: 0%; + } +} + +#filter-checkboxes div { + letter-spacing: .1em; +} + +#filter-checkboxes p { + margin: 0; +} + +main.content-wrapper div.container { + min-width: 100vw; + max-width: 100vw; + margin: 0; +} + +@media screen and (min-width: 992px) { + div.homepage-column { + align-self: stretch; + display: flex; + justify-content: center; + flex-direction: column; + min-height: calc(100vh - var(--header_height)); + /* max-height: calc(100vh - var(--header_height)); */ + } +} + +div#homepage-left-column { + background-color: var(--homepage_left_background); + overflow-y: auto; +} + +.welcome-content-wrapper { + color: var(--home_font_color); + margin: 3rem 2rem; +} + +.welcome-content-wrapper p { + font-family: var(--font-sans-serif-bold); +} + +div.welcome-login-button-wrapper { + margin: 0 2rem; +} + +div.welcome-login-button-wrapper a.btn.welcome-login-btn { + border-color: var(--home_font_color); + color: var(--home_font_color); +} + +div.welcome-login-button-wrapper a.btn.welcome-login-btn span.arrow-right::before, +div.welcome-login-button-wrapper a.btn.welcome-login-btn span.arrow-right, +div.welcome-login-button-wrapper a.btn.welcome-login-btn span.arrow-right::after { + background: var(--home_font_color); +} + +div#homepage-right-column { + background-color: var(--homepage_right_background); + display: flex; + flex-direction: column; + max-height: calc(100vh - var(--header_height)); +} + +div#homepage-right-column::after { + background: var(--homepage_right_background); + bottom: 0; + content: ""; + left: 40%; + position: fixed; + right: 0; + top: 0; + z-index: -1; +} + +div.proj-image-select-wrapper { + max-height: 100%; + display: flex; + flex-grow: 1; + justify-content: center; +} + +.proj-image-select-wrapper img { + object-fit: contain; +} + +div#proj-image-attribution { + color: var(--dark_font); + background-color: var(--accent_bg); + /* background-color: var(--white); */ + border: var(--default_border); + border-radius: var(--default_border_radius); + bottom: .5rem; + opacity: 80%; + /* padding: .375rem .75rem; */ + display: flex; + position: absolute; + right: .5rem; +} + +div#proj-image-attribution .attribution-toggle { + color: #000; + font-family: serif; + font-size: 1.25rem; + line-height: 1; + padding: .375rem .75rem; + text-decoration: none; +} + +div#proj-image-attribution .attribution-toggle.collapsed { + margin-right: 0; +} + +div#proj-image-attribution p { + font-size: var(--small_font_size); + margin: 5px 5px 0 5px; +} + +#collapseAttribution.show:before { + background-color: var(--dark_font); + border: 1px solid #fff; + color: #fff; + content: "X"; + font-family: Tahoma, sans-serif; + font-size: .875rem; + font-weight: bold; + height: 100%; + left: 0; + padding: .25rem .6125rem; + pointer-events: none; + position: absolute; + top: 0; + z-index: 2; +} From 9246a184391a893cbce5fbbac4471cda9d06498d Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Mon, 22 Dec 2025 16:20:05 -0800 Subject: [PATCH 07/11] use react-router for client app --- .gitignore | 5 +- client/app/App.css | 366 ++++++++++++ client/app/api/pageContent.ts | 71 +++ client/{src => app}/components/header.css | 0 client/{src => app}/components/header.tsx | 25 +- client/{src => app}/index.css | 0 client/app/layouts/explore.tsx | 30 + client/app/root.tsx | 116 ++++ client/app/routes.ts | 9 + client/app/routes/about.tsx | 13 + client/app/routes/help.tsx | 13 + client/app/routes/welcome.tsx | 66 +++ client/package-lock.json | 671 ++++++++++++++++++---- client/package.json | 5 +- client/react-router.config.ts | 7 + client/src/App.css | 6 - client/src/App.tsx | 155 ----- client/src/api/pageContent.ts | 28 - client/src/assets/react.svg | 1 - client/src/main.tsx | 11 - client/tsconfig.app.json | 10 +- client/vite.config.ts | 12 +- 22 files changed, 1299 insertions(+), 321 deletions(-) create mode 100644 client/app/App.css create mode 100644 client/app/api/pageContent.ts rename client/{src => app}/components/header.css (100%) rename client/{src => app}/components/header.tsx (85%) rename client/{src => app}/index.css (100%) create mode 100644 client/app/layouts/explore.tsx create mode 100644 client/app/root.tsx create mode 100644 client/app/routes.ts create mode 100644 client/app/routes/about.tsx create mode 100644 client/app/routes/help.tsx create mode 100644 client/app/routes/welcome.tsx create mode 100644 client/react-router.config.ts delete mode 100644 client/src/App.css delete mode 100644 client/src/App.tsx delete mode 100644 client/src/api/pageContent.ts delete mode 100644 client/src/assets/react.svg delete mode 100644 client/src/main.tsx diff --git a/.gitignore b/.gitignore index 4bf0cfc..fb1fed7 100644 --- a/.gitignore +++ b/.gitignore @@ -126,4 +126,7 @@ dist-ssr *.ntvs* *.njsproj *.sln -*.sw? \ No newline at end of file +*.sw? + +# client related files +.react-router/ \ No newline at end of file diff --git a/client/app/App.css b/client/app/App.css new file mode 100644 index 0000000..1852fac --- /dev/null +++ b/client/app/App.css @@ -0,0 +1,366 @@ +#root { + display: flex; + flex-direction: column; + min-height: 100vh; + width: 100%; +} + +:root { + --header_height: 6rem; + --black: #000000; + --white: #ffffff; + /* PRIMARY A: Vivid, unique. ORIGINAL: SIENNA */ + --current_pg_index: var(--primary_a); + --highlight_underline: var(--primary_a); + --highlight_font: var(--primary_a); + /* PRIMARY B VERY faint. ORIGINAL: OFF-WHITE */ + --light_font: var(--primary_b); + --accent_bg: var(--primary_b); + /* PRIMARY C VERY dark, bold. ORIGINAL: OFF-BLACK */ + --dark_font: var(--primary_c); + /* PRIMARY D neutral. ORIGINAL: GRAY */ + --result_highlight: var(--primary_d); + --alt_bg: var(--primary_d); + --light_border: var(--primary_d); + /* SECONDARY A Themed, dark. ORIGINAL: FOREST-GREEN */ + --link_color: var(--secondary_a); + --alt_field_bg: var(--secondary_a); + /* SECONDARY B Themed, moderate. ORIGINAL: OLIVE-GREEN */ + /* SECONDARY C Themed, vivid. ORIGINAL: GREEN */ + --ok_button_bg: var(--secondary_c); + --placeholder_font: var(--secondary_c); + /* SECONDARY D Vivid, unique, accent. ORIGINAL: FUCHSIA*/ + /* PALETTE OVERRIDES */ + --default_background: var(--white); + --default_alt_font: var(--white); + --default_font: var(--black); + --default_border: var(--black); + --btn_border: 2px solid var(--white); + --btn_color: var(--white); + /* Theme Colors */ + --theme_bg_color: var(--black); + --theme_color: var(--white); + /* FONT OVERRIDES */ + --bs-font-sans-serif: "Open Sans Regular", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-sans-serif: var(--bs-font-sans-serif); + --font-sans-serif-bold: "Open Sans Bold", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-sans-serif-extrabold: "Open Sans ExtraBold", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --small_font_size: 0.7rem; + + /* Spacing */ + --margin_bottom: 1.5em; + --margin_top: 1.5em; + --margin: 1.5em; + --letter_spacing: 0.05em; + + /* Borders */ + --default_border: 1px solid var(--default_font); + --default_border_radius: 0.3rem; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} +main { + background: var(--white); +} + +.min-full-height { + min-height: calc(100vh - 96px); +} + +h1 { + display: block; + /* margin: var(--margin_top) 0 calc(var(--margin_bottom) / 2); */ + margin-bottom: 0; + position: relative; + text-align: center; +} + +h2 { + font-size: 2em; + margin-top: var(--margin_top); +} + +p { + font-size: 1.25em; + letter-spacing: .05em; + margin: calc(var(--margin_top) / 2) 0 calc(var(--margin_bottom) * 2) +} + +.explore-form-row { + align-items: stretch; + flex-direction: row; +} + +.explore-form-row > div { + /* width: 100%; */ +} + +.explore-form { + margin-top: calc(var(--margin_top) * 2); +} + +.explore-input-group { + /* margin: var(--margin_top) 0 calc(var(--margin_bottom) * 2); */ + padding: 0 10%; +} + +hr { + font-size: 1.25em; + margin: calc(var(--margin_top) * 2) 0 calc(var(--margin_bottom) * 2); +} + +.explore-form .btn { + /* border: 1px solid var(--primary_d): */ + /* position: relative; */ +} + +.button-wrapper { + /* justify-content: ; */ + margin: calc(var(--margin_top) * 2) 0; + position: relative; + text-align: center; +} + +button.btn { + background: var(--white); + border-color: var(--black); + border-radius: 0; + color: var(--black); + font-family: var(--font-sans-serif); + font-size: 1.0375em; +} + +button.btn:hover { + background: var(--primary_a); + border-color: var(--black); + color: var(--white); +} + +.arrow-right { + display: inline-block; + position: relative; +} + +button.btn .arrow-right, +button.btn .arrow-right::before, +button.btn .arrow-right::after { + background: var(--primary_a); +} + +button.btn:hover .arrow-right, +button.btn:hover .arrow-right::before, +button.btn:hover .arrow-right::after { + background: var(--white); +} + +.form-wrapper { + margin-top: calc(var(--margin_top) * 2); + text-align: center; +} + + +.form-wrapper .explore-form h1:nth-of-type(2)::after { + background: var(--secondary_b); + border-radius: 50%; + color: var(--white); + content: "OR"; + font-size: .3125em; + height: 4em; + left: 50%; + line-height: 4em; + position: absolute; + text-align: center; + top: -5em; + transform: translateX(-50%); + width: 4em; + white-space: nowrap; +} + +#filter-checkboxes { + flex-direction: row; + padding-left: 40%; + text-transform: uppercase; + text-align: left; +} + +@media (min-width: 64em) { + #filter-checkboxes { + padding-left: 0%; + } +} + +#filter-checkboxes div { + letter-spacing: .1em; +} + +#filter-checkboxes p { + margin: 0; +} + +main.content-wrapper div.container { + min-width: 100vw; + max-width: 100vw; + margin: 0; +} + +@media screen and (min-width: 992px) { + div.homepage-column { + align-self: stretch; + display: flex; + justify-content: center; + flex-direction: column; + min-height: calc(100vh - var(--header_height)); + /* max-height: calc(100vh - var(--header_height)); */ + } +} + +div#homepage-left-column { + background-color: var(--homepage_left_background); + overflow-y: auto; +} + +.welcome-content-wrapper { + color: var(--home_font_color); + margin: 3rem 2rem; +} + +.welcome-content-wrapper p { + font-family: var(--font-sans-serif-bold); +} + +div.welcome-login-button-wrapper { + margin: 0 2rem; +} + +div.welcome-login-button-wrapper a.btn.welcome-login-btn { + border-color: var(--home_font_color); + color: var(--home_font_color); +} + +div.welcome-login-button-wrapper a.btn.welcome-login-btn span.arrow-right::before, +div.welcome-login-button-wrapper a.btn.welcome-login-btn span.arrow-right, +div.welcome-login-button-wrapper a.btn.welcome-login-btn span.arrow-right::after { + background: var(--home_font_color); +} + +div#homepage-right-column { + background-color: var(--homepage_right_background); + display: flex; + flex-direction: column; + max-height: calc(100vh - var(--header_height)); +} + +div#homepage-right-column::after { + background: var(--homepage_right_background); + bottom: 0; + content: ""; + left: 40%; + position: fixed; + right: 0; + top: 0; + z-index: -1; +} + +div.proj-image-select-wrapper { + max-height: 100%; + display: flex; + flex-grow: 1; + justify-content: center; +} + +.proj-image-select-wrapper img { + object-fit: contain; +} + +div#proj-image-attribution { + color: var(--dark_font); + background-color: var(--accent_bg); + /* background-color: var(--white); */ + border: var(--default_border); + border-radius: var(--default_border_radius); + bottom: .5rem; + opacity: 80%; + /* padding: .375rem .75rem; */ + display: flex; + position: absolute; + right: .5rem; +} + +div#proj-image-attribution .attribution-toggle { + color: #000; + font-family: serif; + font-size: 1.25rem; + line-height: 1; + padding: .375rem .75rem; + text-decoration: none; +} + +div#proj-image-attribution .attribution-toggle.collapsed { + margin-right: 0; +} + +div#proj-image-attribution p { + font-size: var(--small_font_size); + margin: 5px 5px 0 5px; +} + +#collapseAttribution.show:before { + background-color: var(--dark_font); + border: 1px solid #fff; + color: #fff; + content: "X"; + font-family: Tahoma, sans-serif; + font-size: .875rem; + font-weight: bold; + height: 100%; + left: 0; + padding: .25rem .6125rem; + pointer-events: none; + position: absolute; + top: 0; + z-index: 2; +} \ No newline at end of file diff --git a/client/app/api/pageContent.ts b/client/app/api/pageContent.ts new file mode 100644 index 0000000..63451b4 --- /dev/null +++ b/client/app/api/pageContent.ts @@ -0,0 +1,71 @@ +import { useOutletContext } from "react-router"; + +export type PageContentResponse = { + page: string; + pageTitle: string; + pageContent: string; +}; + +export type SiteInfoResponse = { + proj_logo_text: string; + proj_text_placement: string; + proj_css: Record; + proj_icons: { + logo: string; + place_icon: string; + resource_icon: string; + activity_icon: string; + source_icon: string; + media_icon: string; + }; + proj_image_select: string; + home_image_attribution: string; + home_font_color: string; + homepage_right_background: string; + homepage_left_background: string; + map_pin: string; + map_pin_selected: string; +}; + +export type PageContentAndSiteInfo = { + pageContent: PageContentResponse; + siteInfo: SiteInfoResponse; +}; + +// TODO: fix baseUrl to use env variable +const baseUrl = "http://localhost:8000"; + +export async function fetchPageContent( + path: string +): Promise { + const url = `${baseUrl}/api/page${path}/`; + const res = await fetch(url, { credentials: "include" }); + if (!res.ok) { + throw new Error(`Failed to fetch page content: ${res.status}`); + } + return res.json(); +} + +export async function fetchSiteInfo(): Promise { + const url = `${baseUrl}/api/site-info/`; + const res = await fetch(url, { credentials: "include" }); + if (!res.ok) { + throw new Error(`Failed to fetch site info: ${res.status}`); + } + return await res.json(); +} + +export async function fetchPageContentAndSiteInfo(path: string): Promise<{ + pageContent: PageContentResponse; + siteInfo: SiteInfoResponse; +}> { + const [pageContent, siteInfo] = await Promise.all([ + fetchPageContent(path), + fetchSiteInfo(), + ]); + return { pageContent, siteInfo }; +} + +export const usePageContentAndSiteInfo = () => { + return useOutletContext(); +}; diff --git a/client/src/components/header.css b/client/app/components/header.css similarity index 100% rename from client/src/components/header.css rename to client/app/components/header.css diff --git a/client/src/components/header.tsx b/client/app/components/header.tsx similarity index 85% rename from client/src/components/header.tsx rename to client/app/components/header.tsx index 639da24..0382b92 100644 --- a/client/src/components/header.tsx +++ b/client/app/components/header.tsx @@ -1,23 +1,22 @@ import React from "react"; +import { Link } from "react-router"; import "./header.css"; -interface HeaderProps { +export type HeaderProps = { projIcons: { logo: string; }; projTextPlacement: string; projLogoText: string; pageTitle: string; - setPageName: (name: "Welcome" | "About" | "Help") => void; isAuthenticated?: boolean; -} +}; const Header: React.FC = ({ projIcons, projTextPlacement, projLogoText, pageTitle, - setPageName, isAuthenticated, }) => { // TODO: fix hardcoded URL @@ -55,19 +54,22 @@ const Header: React.FC = ({
  • - setPageName("About")} className="nav-link"> + About - +
  • <> {isAuthenticated ? ( - + Search - + ) : ( + // + // Search + // Search )} @@ -75,9 +77,12 @@ const Header: React.FC = ({
  • - setPageName("Help")} className="nav-link"> + + Help + + {/* setPageName("Help")} className="nav-link"> Help - + */}
  • {isAuthenticated ? (
  • diff --git a/client/src/index.css b/client/app/index.css similarity index 100% rename from client/src/index.css rename to client/app/index.css diff --git a/client/app/layouts/explore.tsx b/client/app/layouts/explore.tsx new file mode 100644 index 0000000..41a29c8 --- /dev/null +++ b/client/app/layouts/explore.tsx @@ -0,0 +1,30 @@ +import type React from "react"; +import { Outlet } from "react-router"; +import Header from "../components/header"; +import { usePageContentAndSiteInfo } from "../api/pageContent"; + +const ExploreLayout: React.FC = () => { + const { pageContent, siteInfo } = usePageContentAndSiteInfo(); + const isAuthenticated = false; // TODO: Replace with actual authentication logic + + return ( + <> +
    +
    +
    +
    + +
    +
    +
    + + ); +}; + +export default ExploreLayout; diff --git a/client/app/root.tsx b/client/app/root.tsx new file mode 100644 index 0000000..6191d24 --- /dev/null +++ b/client/app/root.tsx @@ -0,0 +1,116 @@ +import { + isRouteErrorResponse, + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, + useLoaderData, +} from "react-router"; + +import type { Route } from "./+types/root"; +import "./App.css"; +import "bootstrap/dist/css/bootstrap.min.css"; +import { + fetchPageContentAndSiteInfo, + type PageContentAndSiteInfo, +} from "./api/pageContent"; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export const RouteToPath: Record = { + about: "/About", + help: "/Help", + welcome: "/Welcome", +}; + +export async function loader({ request }: Route.LoaderArgs) { + const url = new URL(request.url); + const [, path] = url.pathname.split("/"); + const pageContent = await fetchPageContentAndSiteInfo( + RouteToPath[path as keyof typeof RouteToPath] || "/Welcome" + ); + return pageContent; +} + +export default function Root() { + const { pageContent, siteInfo } = useLoaderData(); + + return ( + <> +