commit 0c7aa53c8bd753aaf8af3a940c2948e9f5b45d18 Author: Hanzo_dev <2002samudiojohan@gmail.com> Date: Sat Feb 21 09:53:31 2026 -0500 Initial commit: SIBU 2.0 MISSION diff --git a/.cursor/rules/main.mdc b/.cursor/rules/main.mdc new file mode 100644 index 0000000..0edd019 --- /dev/null +++ b/.cursor/rules/main.mdc @@ -0,0 +1,113 @@ +# SIBU Project Rules and Conventions + +## Project Structure + +This is a full-stack transportation application: +- **Backend**: FastAPI + SQLModel + Postgres (in `backend/` folder) using uv +- **Frontend**: Vue3 + Vite + Shadcn (in `frontend/` folder) using Bun + +## Package Management + +### Backend +- **Always use `uv` for package management** +- **NEVER edit `pyproject.toml` manually** - use `uv add ` instead +- Execute backend with: `uv run fastapi dev app/main.py` (development) or `uv run fastapi run app/main.py` (production) +- Run migrations with: `uv run alembic upgrade head` + +### Frontend +- **Always use `bun` for package management** +- **NEVER edit `package.json` manually** - use `bun add ` instead +- Run development server: `bun run dev` +- Build for production: `bun run build` + +## Backend Conventions + +### Configuration +- Use `pydantic-settings` BaseSettings class in `app/core/config.py` for all environment variable management +- Environment files: `.env.development` and `.env.production` +- Database URL format: `postgresql+asyncpg://localhost:5432/sibu_dev` + +### Code Style +- Use Python type hints for all function parameters and return types +- Use SQLModel for database models +- Use Pydantic schemas for request/response validation +- Follow FastAPI best practices for API routes +- Use dependency injection for database sessions + +### Project Structure +``` +backend/ + ├── app/ + │ ├── api/ # API route handlers + │ ├── core/ # Configuration and database + │ ├── models/ # SQLModel models + │ ├── schemas/ # Pydantic schemas + │ └── services/ # Business logic + ├── alembic/ # Database migrations + └── pyproject.toml # Managed by uv +``` + +## Frontend Conventions + +### Code Style +- Use Vue3 Composition API with ` + + + \ No newline at end of file diff --git a/frontend/nixpacks.toml b/frontend/nixpacks.toml new file mode 100644 index 0000000..404784c --- /dev/null +++ b/frontend/nixpacks.toml @@ -0,0 +1,5 @@ +[phases.setup] +nixPkgs = ["nodejs_22", "bun"] + +[phases.build] +cmds = ["bun install", "bun run build"] diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..0bfb3ab --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,8466 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@capacitor/android": "^8.0.2", + "@capacitor/cli": "^8.0.0", + "@capacitor/core": "^8.0.0", + "@capacitor/geolocation": "^8.0.0", + "@googlemaps/js-api-loader": "^2.0.2", + "axios": "^1.13.2", + "chart.js": "^4.5.1", + "html2canvas": "^1.4.1", + "jspdf": "^4.1.0", + "pinia": "^3.0.4", + "vue": "^3.5.24", + "vue-chartjs": "^5.3.3", + "vue-i18n": "^9.14.5", + "vue-router": "^4.6.3" + }, + "devDependencies": { + "@types/google.maps": "^3.58.1", + "@types/node": "^24.10.1", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.8.1", + "typescript": "~5.9.3", + "vite": "^7.2.4", + "vite-plugin-pwa": "^1.2.0", + "vite-plugin-vue-devtools": "^8.0.5", + "vue-tsc": "^3.1.4" + } + }, + "node_modules/@apideck/better-ajv-errors": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", + "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-schema": "^0.4.0", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "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-create-class-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", + "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.10" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.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-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "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-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "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/helper-wrap-function": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", + "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2" + }, + "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", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.0.tgz", + "integrity": "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-decorators": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz", + "integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", + "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz", + "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", + "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", + "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", + "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz", + "integrity": "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz", + "integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.5", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.4", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/plugin-transform-exponentiation-operator": "^7.28.5", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.28.5", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.28.4", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.4", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.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", + "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", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@capacitor/android": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.0.2.tgz", + "integrity": "sha512-0D7j0YvzjnfCMKLvFkAbx8b3Vwx+QfHFG5NzoXpI9sAl3zWiLsfa+NX4x92Fy+k4MGjLSMAfLThCqILYGDDsgw==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^8.0.0" + } + }, + "node_modules/@capacitor/cli": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.0.1.tgz", + "integrity": "sha512-okCNTsL8FNYrtPNeHWFjWb1S+PwBMhx5wFLhDC0MZOIrOLm+2ynMBtKu3BnR0Nv1hozoHcOCi6SuTF1TrRpb3w==", + "license": "MIT", + "dependencies": { + "@ionic/cli-framework-output": "^2.2.8", + "@ionic/utils-subprocess": "^3.0.1", + "@ionic/utils-terminal": "^2.3.5", + "commander": "^12.1.0", + "debug": "^4.4.0", + "env-paths": "^2.2.0", + "fs-extra": "^11.2.0", + "kleur": "^4.1.5", + "native-run": "^2.0.3", + "open": "^8.4.0", + "plist": "^3.1.0", + "prompts": "^2.4.2", + "rimraf": "^6.0.1", + "semver": "^7.6.3", + "tar": "^6.1.11", + "tslib": "^2.8.1", + "xml2js": "^0.6.2" + }, + "bin": { + "cap": "bin/capacitor", + "capacitor": "bin/capacitor" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@capacitor/cli/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@capacitor/cli/node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@capacitor/cli/node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@capacitor/cli/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@capacitor/cli/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@capacitor/cli/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@capacitor/cli/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@capacitor/core": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.0.1.tgz", + "integrity": "sha512-5UqSWxGMp/B8KhYu7rAijqNtYslhcLh+TrbfU48PfdMDsPfaU/VY48sMNzC22xL8BmoFoql/3SKyP+pavTOvOA==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@capacitor/geolocation": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@capacitor/geolocation/-/geolocation-8.0.0.tgz", + "integrity": "sha512-ci6gOWd5/uruC3bRTxI3xFd9g82WnnJbdQxOH3oZOATNuedlaAw5c5/zXEIMYfRB7t2x1v8/N9gXkND6/nOmVQ==", + "license": "MIT", + "dependencies": { + "@capacitor/synapse": "^1.0.4" + }, + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/synapse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@capacitor/synapse/-/synapse-1.0.4.tgz", + "integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==", + "license": "ISC" + }, + "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", + "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/@googlemaps/js-api-loader": { + "version": "2.0.2", + "license": "Apache-2.0", + "dependencies": { + "@types/google.maps": "^3.53.1" + } + }, + "node_modules/@intlify/core-base": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.5.tgz", + "integrity": "sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==", + "license": "MIT", + "dependencies": { + "@intlify/message-compiler": "9.14.5", + "@intlify/shared": "9.14.5" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.5.tgz", + "integrity": "sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==", + "license": "MIT", + "dependencies": { + "@intlify/shared": "9.14.5", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.5.tgz", + "integrity": "sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@ionic/cli-framework-output": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz", + "integrity": "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==", + "license": "MIT", + "dependencies": { + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-array": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", + "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==", + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz", + "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==", + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.0", + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-object": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz", + "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==", + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-process": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz", + "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==", + "license": "MIT", + "dependencies": { + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-process/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/@ionic/utils-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz", + "integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==", + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-subprocess": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz", + "integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==", + "license": "MIT", + "dependencies": { + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.12", + "@ionic/utils-stream": "3.1.7", + "@ionic/utils-terminal": "2.3.5", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz", + "integrity": "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==", + "license": "MIT", + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ionic/utils-terminal/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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@ionic/utils-terminal/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/@ionic/utils-terminal/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/@ionic/utils-terminal/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ionic/utils-terminal/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ionic/utils-terminal/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "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/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.50", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "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", + "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/@surma/rollup-plugin-off-main-thread": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ejs": "^3.1.6", + "json5": "^2.2.0", + "magic-string": "^0.25.0", + "string.prototype.matchall": "^4.0.6" + } + }, + "node_modules/@surma/rollup-plugin-off-main-thread/node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/google.maps": { + "version": "3.58.1", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", + "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.1", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.50" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.23", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.23" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.23", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.23", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", + "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz", + "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", + "@vue/babel-helper-vue-transform-on": "1.5.0", + "@vue/babel-plugin-resolve-type": "1.5.0", + "@vue/shared": "^3.5.18" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz", + "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/parser": "^7.28.0", + "@vue/compiler-sfc": "^3.5.18" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.25", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.25", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.25", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.25", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.25", + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.25", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-core": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-8.0.5.tgz", + "integrity": "sha512-dpCw8nl0GDBuiL9SaY0mtDxoGIEmU38w+TQiYEPOLhW03VDC0lfNMYXS/qhl4I0YlysGp04NLY4UNn6xgD0VIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.0.5", + "@vue/devtools-shared": "^8.0.5", + "mitt": "^3.0.1", + "nanoid": "^5.1.5", + "pathe": "^2.0.3", + "vite-hot-client": "^2.1.0" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@vue/devtools-core/node_modules/@vue/devtools-kit": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.5.tgz", + "integrity": "sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.0.5", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^2.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-core/node_modules/@vue/devtools-shared": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.5.tgz", + "integrity": "sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/devtools-core/node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@vue/devtools-core/node_modules/perfect-debounce": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz", + "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/language-core": { + "version": "3.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.0.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.25", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.25", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.25", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.25", + "@vue/runtime-core": "3.5.25", + "@vue/shared": "3.5.25", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.25", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25" + }, + "peerDependencies": { + "vue": "3.5.25" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.25", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.8.1", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/alien-signals": { + "version": "3.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.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/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz", + "integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/birpc": { + "version": "2.8.0", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "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/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001757", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", + "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "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/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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==", + "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==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "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/copy-anything": { + "version": "4.0.5", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/core-js": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", + "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", + "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "license": "MIT" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", + "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.263", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.263.tgz", + "integrity": "sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==", + "dev": true, + "license": "ISC" + }, + "node_modules/elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==", + "license": "Apache-2.0", + "dependencies": { + "sax": "1.1.4" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "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/estree-walker": { + "version": "2.0.2", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true, + "license": "ISC" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "license": "MIT" + }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/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-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "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/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jspdf": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.1.0.tgz", + "integrity": "sha512-xd1d/XRkwqnsq6FP3zH1Q+Ejqn2ULIJeDZ+FTKpaabVpZREjsJKRJwuokTNgdqOU+fl55KgbvgZ1pRTSWCP2kQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "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/magic-string": { + "version": "0.30.21", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/mitt": { + "version": "3.0.1", + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "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/native-run": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.3.tgz", + "integrity": "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==", + "license": "MIT", + "dependencies": { + "@ionic/utils-fs": "^3.1.7", + "@ionic/utils-terminal": "^2.3.4", + "bplist-parser": "^0.3.2", + "debug": "^4.3.4", + "elementtree": "^0.1.7", + "ini": "^4.1.1", + "plist": "^3.1.0", + "split2": "^4.2.0", + "through2": "^4.0.2", + "tslib": "^2.6.2", + "yauzl": "^2.10.0" + }, + "bin": { + "native-run": "bin/native-run" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "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/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "license": "MIT" + }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, + "node_modules/rimraf": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "dev": true, + "license": "MIT", + "peer": true, + "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/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sax": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==", + "license": "ISC" + }, + "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/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true, + "license": "MIT" + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.44.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "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/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "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-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unplugin-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz", + "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/vite": { + "version": "7.2.4", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-dev-rpc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vite-dev-rpc/-/vite-dev-rpc-1.1.0.tgz", + "integrity": "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==", + "dev": true, + "license": "MIT", + "dependencies": { + "birpc": "^2.4.0", + "vite-hot-client": "^2.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0" + } + }, + "node_modules/vite-hot-client": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-2.1.0.tgz", + "integrity": "sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" + } + }, + "node_modules/vite-plugin-inspect": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-11.3.3.tgz", + "integrity": "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.1.0", + "debug": "^4.4.1", + "error-stack-parser-es": "^1.0.5", + "ohash": "^2.0.11", + "open": "^10.2.0", + "perfect-debounce": "^2.0.0", + "sirv": "^3.0.1", + "unplugin-utils": "^0.3.0", + "vite-dev-rpc": "^1.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite-plugin-inspect/node_modules/perfect-debounce": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz", + "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite-plugin-pwa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.2.0.tgz", + "integrity": "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.6", + "pretty-bytes": "^6.1.1", + "tinyglobby": "^0.2.10", + "workbox-build": "^7.4.0", + "workbox-window": "^7.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vite-pwa/assets-generator": "^1.0.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", + "workbox-build": "^7.4.0", + "workbox-window": "^7.4.0" + }, + "peerDependenciesMeta": { + "@vite-pwa/assets-generator": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vue-devtools": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.0.5.tgz", + "integrity": "sha512-p619BlKFOqQXJ6uDWS1vUPQzuJOD6xJTfftj57JXBGoBD/yeQCowR7pnWcr/FEX4/HVkFbreI6w2uuGBmQOh6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-core": "^8.0.5", + "@vue/devtools-kit": "^8.0.5", + "@vue/devtools-shared": "^8.0.5", + "sirv": "^3.0.2", + "vite-plugin-inspect": "^11.3.3", + "vite-plugin-vue-inspector": "^5.3.2" + }, + "engines": { + "node": ">=v14.21.3" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0-0" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/@vue/devtools-kit": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.5.tgz", + "integrity": "sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.0.5", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^2.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/@vue/devtools-shared": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.5.tgz", + "integrity": "sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/perfect-debounce": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz", + "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite-plugin-vue-inspector": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.3.2.tgz", + "integrity": "sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.0", + "@babel/plugin-proposal-decorators": "^7.23.0", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.22.15", + "@vue/babel-plugin-jsx": "^1.1.5", + "@vue/compiler-dom": "^3.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.4" + }, + "peerDependencies": { + "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.25", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-sfc": "3.5.25", + "@vue/runtime-dom": "3.5.25", + "@vue/server-renderer": "3.5.25", + "@vue/shared": "3.5.25" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-chartjs": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.3.tgz", + "integrity": "sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "vue": "^3.0.0-0 || ^2.7.0" + } + }, + "node_modules/vue-i18n": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.5.tgz", + "integrity": "sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "9.14.5", + "@intlify/shared": "9.14.5", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vue-i18n/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/vue-router": { + "version": "4.6.3", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "license": "MIT" + }, + "node_modules/vue-tsc": { + "version": "3.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.23", + "@vue/language-core": "3.1.5" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/workbox-background-sync": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.0.tgz", + "integrity": "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.0.tgz", + "integrity": "sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-build": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.0.tgz", + "integrity": "sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.24.4", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^5.2.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^2.4.1", + "@rollup/plugin-terser": "^0.4.3", + "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^11.0.1", + "lodash": "^4.17.20", + "pretty-bytes": "^5.3.0", + "rollup": "^2.79.2", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "7.4.0", + "workbox-broadcast-update": "7.4.0", + "workbox-cacheable-response": "7.4.0", + "workbox-core": "7.4.0", + "workbox-expiration": "7.4.0", + "workbox-google-analytics": "7.4.0", + "workbox-navigation-preload": "7.4.0", + "workbox-precaching": "7.4.0", + "workbox-range-requests": "7.4.0", + "workbox-recipes": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0", + "workbox-streams": "7.4.0", + "workbox-sw": "7.4.0", + "workbox-window": "7.4.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/workbox-build/node_modules/@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + } + } + }, + "node_modules/workbox-build/node_modules/@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/workbox-build/node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/workbox-build/node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-build/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-build/node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/workbox-build/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/workbox-build/node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/workbox-build/node_modules/rollup": { + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.0.tgz", + "integrity": "sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-core": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.0.tgz", + "integrity": "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-expiration": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.0.tgz", + "integrity": "sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-google-analytics": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.0.tgz", + "integrity": "sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-background-sync": "7.4.0", + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.0.tgz", + "integrity": "sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-precaching": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.0.tgz", + "integrity": "sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" + } + }, + "node_modules/workbox-range-requests": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.0.tgz", + "integrity": "sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-recipes": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.0.tgz", + "integrity": "sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-cacheable-response": "7.4.0", + "workbox-core": "7.4.0", + "workbox-expiration": "7.4.0", + "workbox-precaching": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" + } + }, + "node_modules/workbox-routing": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.0.tgz", + "integrity": "sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-strategies": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.0.tgz", + "integrity": "sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-streams": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.0.tgz", + "integrity": "sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0" + } + }, + "node_modules/workbox-sw": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.0.tgz", + "integrity": "sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-window": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.0.tgz", + "integrity": "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "7.4.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "license": "MIT", + "engines": { + "node": ">=8.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/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..895f3a3 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,38 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@capacitor/android": "^8.0.2", + "@capacitor/cli": "^8.0.0", + "@capacitor/core": "^8.0.0", + "@capacitor/geolocation": "^8.0.0", + "@googlemaps/js-api-loader": "^2.0.2", + "axios": "^1.13.2", + "chart.js": "^4.5.1", + "html2canvas": "^1.4.1", + "jspdf": "^4.1.0", + "pinia": "^3.0.4", + "vue": "^3.5.24", + "vue-chartjs": "^5.3.3", + "vue-i18n": "^9.14.5", + "vue-router": "^4.6.3" + }, + "devDependencies": { + "@types/google.maps": "^3.58.1", + "@types/node": "^24.10.1", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.8.1", + "typescript": "~5.9.3", + "vite": "^7.2.4", + "vite-plugin-pwa": "^1.2.0", + "vite-plugin-vue-devtools": "^8.0.5", + "vue-tsc": "^3.1.4" + } +} diff --git a/frontend/public/default-coupon.png b/frontend/public/default-coupon.png new file mode 100644 index 0000000..78e5aa6 Binary files /dev/null and b/frontend/public/default-coupon.png differ diff --git a/frontend/public/favicon-16.png b/frontend/public/favicon-16.png new file mode 100644 index 0000000..39bd70c Binary files /dev/null and b/frontend/public/favicon-16.png differ diff --git a/frontend/public/favicon-32.png b/frontend/public/favicon-32.png new file mode 100644 index 0000000..0844afb Binary files /dev/null and b/frontend/public/favicon-32.png differ diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..6513e6e Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/icon-1024.png b/frontend/public/icon-1024.png new file mode 100644 index 0000000..0c495bf Binary files /dev/null and b/frontend/public/icon-1024.png differ diff --git a/frontend/public/icon-192.png b/frontend/public/icon-192.png new file mode 100644 index 0000000..c2055f7 Binary files /dev/null and b/frontend/public/icon-192.png differ diff --git a/frontend/public/icon-512.png b/frontend/public/icon-512.png new file mode 100644 index 0000000..ed09f7f Binary files /dev/null and b/frontend/public/icon-512.png differ diff --git a/frontend/public/icon.svg b/frontend/public/icon.svg new file mode 100644 index 0000000..ec20ec6 --- /dev/null +++ b/frontend/public/icon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/public/sibu.png b/frontend/public/sibu.png new file mode 100644 index 0000000..c2b3cd3 Binary files /dev/null and b/frontend/public/sibu.png differ diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..bb38f07 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/frontend/src/assets/vue.svg b/frontend/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/frontend/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/AppHeader.vue b/frontend/src/components/AppHeader.vue new file mode 100644 index 0000000..6d411ca --- /dev/null +++ b/frontend/src/components/AppHeader.vue @@ -0,0 +1,583 @@ + + + + + diff --git a/frontend/src/components/BottomNav.vue b/frontend/src/components/BottomNav.vue new file mode 100644 index 0000000..bba054f --- /dev/null +++ b/frontend/src/components/BottomNav.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/frontend/src/components/BusStopEditor.vue b/frontend/src/components/BusStopEditor.vue new file mode 100644 index 0000000..7ee781d --- /dev/null +++ b/frontend/src/components/BusStopEditor.vue @@ -0,0 +1,267 @@ + + + + + diff --git a/frontend/src/components/BusStopInfoModal.vue b/frontend/src/components/BusStopInfoModal.vue new file mode 100644 index 0000000..7a401f5 --- /dev/null +++ b/frontend/src/components/BusStopInfoModal.vue @@ -0,0 +1,442 @@ + + + + + diff --git a/frontend/src/components/FavoriteButton.vue b/frontend/src/components/FavoriteButton.vue new file mode 100644 index 0000000..7fc1a5d --- /dev/null +++ b/frontend/src/components/FavoriteButton.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/frontend/src/components/HelloWorld.vue b/frontend/src/components/HelloWorld.vue new file mode 100644 index 0000000..b58e52b --- /dev/null +++ b/frontend/src/components/HelloWorld.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/frontend/src/components/ReportModal.vue b/frontend/src/components/ReportModal.vue new file mode 100644 index 0000000..3711083 --- /dev/null +++ b/frontend/src/components/ReportModal.vue @@ -0,0 +1,285 @@ + + + + + diff --git a/frontend/src/components/auth/LoginForm.vue b/frontend/src/components/auth/LoginForm.vue new file mode 100644 index 0000000..7e43672 --- /dev/null +++ b/frontend/src/components/auth/LoginForm.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/frontend/src/components/auth/RegisterForm.vue b/frontend/src/components/auth/RegisterForm.vue new file mode 100644 index 0000000..b0c615c --- /dev/null +++ b/frontend/src/components/auth/RegisterForm.vue @@ -0,0 +1,255 @@ + + + + + diff --git a/frontend/src/components/common/OffersBadge.vue b/frontend/src/components/common/OffersBadge.vue new file mode 100644 index 0000000..297e801 --- /dev/null +++ b/frontend/src/components/common/OffersBadge.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/frontend/src/components/common/ThemeToggle.vue b/frontend/src/components/common/ThemeToggle.vue new file mode 100644 index 0000000..f24637f --- /dev/null +++ b/frontend/src/components/common/ThemeToggle.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/frontend/src/components/common/UserSonar.vue b/frontend/src/components/common/UserSonar.vue new file mode 100644 index 0000000..d22b0b1 --- /dev/null +++ b/frontend/src/components/common/UserSonar.vue @@ -0,0 +1,67 @@ + + + diff --git a/frontend/src/components/layouts/MainLayout.vue b/frontend/src/components/layouts/MainLayout.vue new file mode 100644 index 0000000..6b3eceb --- /dev/null +++ b/frontend/src/components/layouts/MainLayout.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/frontend/src/composables/useGoogleMaps.ts b/frontend/src/composables/useGoogleMaps.ts new file mode 100644 index 0000000..cf75ceb --- /dev/null +++ b/frontend/src/composables/useGoogleMaps.ts @@ -0,0 +1,387 @@ +/** Composable for Google Maps integration */ +import { ref, onMounted } from 'vue' +import { setOptions, importLibrary } from '@googlemaps/js-api-loader' + +const getApiKey = () => import.meta.env.VITE_GOOGLE_MAPS_API_KEY || '' + +let mapsLoaded = false + +// Global overlay tracker - persists across all composable instances +const globalOverlays = new Map>() + +export function useGoogleMaps() { + const map = ref(null) + const isLoaded = ref(false) + const error = ref(null) + + // Escuchar errores globales de autenticación de Google + if (typeof window !== 'undefined') { + (window as any).gm_auth_failure = () => { + error.value = '⚠️ Error de Autenticación de Google: Revisa que la API de Mapas esté activada y que la facturación de Google Cloud sea válida.'; + console.error('❌ Google Maps Auth Failure detected'); + }; + } + + async function loadMaps() { + if (mapsLoaded) { + isLoaded.value = true + error.value = null + return + } + + const apiKey = getApiKey() + if (!apiKey || apiKey.length < 10) { + error.value = '❌ Error: VITE_GOOGLE_MAPS_API_KEY no detectada o es inválida.' + console.error(error.value) + return + } + + console.log('🌐 Usando Nueva API Funcional de Google Maps...'); + + try { + // Configuramos las opciones globales como pide el error + setOptions({ + key: apiKey, + v: 'weekly' + }); + + // Cargamos las librerías necesarias una por una + console.log('🛰️ Cargando librerías...'); + await importLibrary('maps'); + await importLibrary('places'); + await importLibrary('geometry'); + + if (typeof google === 'undefined' || !google.maps) { + throw new Error('Google Maps se cargó pero el espacio de nombres "google.maps" no está disponible.'); + } + + mapsLoaded = true + isLoaded.value = true + error.value = null + console.log('✅ Google Maps (New API) cargado con éxito'); + } catch (e: any) { + console.error('❌ Error crítico en Nueva API:', e) + + let msg = 'Error de carga.' + const errStr = String(e).toLowerCase() + + if (errStr.includes('apiprojectmaperror')) { + msg = 'Error de Proyecto: API no habilitada o llave incorrecta.' + } else if (errStr.includes('billing')) { + msg = 'Facturación: Revisa tu cuenta en Google Cloud Console.' + } else if (errStr.includes('referer') || errStr.includes('origin')) { + msg = 'Restricción de Origen: La llave no permite peticiones desde esta App.' + } else { + msg = `Detalle: ${e.message || e}` + } + + error.value = `⚠️ Google Maps: ${msg}` + } + } + + function initMap( + containerId: string, + center: { lat: number; lng: number }, + zoom: number = 12 + ) { + if (!isLoaded.value) { + console.error('Google Maps not loaded yet') + return + } + + const container = document.getElementById(containerId) + if (!container) { + console.error(`Map container with id "${containerId}" not found`) + return + } + + // Clear any existing overlays for this map before creating a new one + if (map.value && globalOverlays.has(map.value)) { + clearAllOverlaysForMap(map.value) + } + + try { + map.value = new google.maps.Map(container, { + center, + zoom, + disableDefaultUI: true, + }) + } catch (e: any) { + console.error('❌ Error inicializando el objeto Map:', e); + error.value = `Error de inicialización: ${e.message || e}`; + } + + // Initialize overlay tracking for this map + if (map.value && !globalOverlays.has(map.value)) { + globalOverlays.set(map.value, new Set()) + } + } + + function addMarker( + position: { lat: number; lng: number }, + options?: { + title?: string + draggable?: boolean + icon?: google.maps.Icon | google.maps.Symbol | string + onDragEnd?: (pos: { lat: number; lng: number }) => void + } + ): google.maps.Marker | null { + if (!map.value) { + console.error('Map not initialized') + return null + } + + const marker = new google.maps.Marker({ + position, + map: map.value, + title: options?.title, + draggable: options?.draggable, + icon: options?.icon, + }) + + if (options?.onDragEnd) { + marker.addListener('dragend', () => { + const pos = marker.getPosition() + if (pos) { + options.onDragEnd!({ lat: pos.lat(), lng: pos.lng() }) + } + }) + } + + // Track in global overlay tracker + if (map.value) { + if (!globalOverlays.has(map.value)) { + globalOverlays.set(map.value, new Set()) + } + globalOverlays.get(map.value)!.add(marker) + } + + return marker + } + + function addNumberedMarker( + position: { lat: number; lng: number }, + number: number, + title?: string, + onClick?: () => void + ): google.maps.Marker | null { + if (!map.value) { + console.error('Map not initialized') + return null + } + + // Note: google.maps.Marker is deprecated but still works + // We'll keep using it for now as AdvancedMarkerElement requires additional setup + // TODO: Migrate to google.maps.marker.AdvancedMarkerElement in the future + const marker = new google.maps.Marker({ + position, + map: map.value, + title, + icon: { + path: google.maps.SymbolPath.CIRCLE, + fillColor: '#FEE715', // Amarillo marca + fillOpacity: 1, + strokeColor: '#101820', // Negro marca + strokeWeight: 2, + scale: 14, + }, + label: { + text: number.toString(), + color: '#101820', + fontSize: '13px', + fontWeight: '900', + }, + }) + + if (onClick) { + marker.addListener('click', onClick) + } + + // Track in global overlay tracker + if (map.value) { + if (!globalOverlays.has(map.value)) { + globalOverlays.set(map.value, new Set()) + } + globalOverlays.get(map.value)!.add(marker) + } + + return marker + } + + function addPolyline(path: Array<{ lat: number; lng: number }>): google.maps.Polyline | null { + if (!map.value) { + console.error('Map not initialized') + return null + } + + const polyline = new google.maps.Polyline({ + path, + geodesic: true, + strokeColor: '#101820', // Negro premium + strokeOpacity: 0.8, + strokeWeight: 5, + map: map.value, + }) + + // Track in global overlay tracker + if (map.value) { + if (!globalOverlays.has(map.value)) { + globalOverlays.set(map.value, new Set()) + } + globalOverlays.get(map.value)!.add(polyline) + } + + return polyline + } + + function fitBounds(path: Array<{ lat: number; lng: number }>) { + if (!map.value || path.length === 0) { + return + } + + const bounds = new google.maps.LatLngBounds() + path.forEach((point) => { + bounds.extend(new google.maps.LatLng(point.lat, point.lng)) + }) + map.value.fitBounds(bounds) + } + + function setCenter(lat: number, lng: number) { + if (map.value) { + map.value.setCenter({ lat, lng }) + } + } + + function setZoom(zoom: number) { + if (map.value) { + map.value.setZoom(zoom) + } + } + + function clearAllOverlays() { + if (!map.value) { + return + } + clearAllOverlaysForMap(map.value) + } + + function clearAllOverlaysForMap(targetMap: google.maps.Map) { + const overlays = globalOverlays.get(targetMap) + + // Remove all tracked overlays from the map + if (overlays) { + const overlayCount = overlays.size + overlays.forEach((overlay) => { + if (overlay) { + try { + if ('setMap' in overlay && typeof overlay.setMap === 'function') { + overlay.setMap(null) + } + if ('remove' in overlay && typeof overlay.remove === 'function') { + overlay.remove() + } + } catch (e) { + // Ignore errors when removing overlays + console.warn('Error removing overlay:', e) + } + } + }) + // Clear the set + overlays.clear() + console.log(`Cleared ${overlayCount} tracked overlays`) + } + + // Manual DOM scraping fallback removed as it causes "removeChild" errors + // with Google Maps' native OverlayView management. + } + + function addHtmlMarker( + position: { lat: number; lng: number }, + htmlContent: string, + offset: { x: number; y: number } = { x: 0, y: 0 } + ) { + if (!map.value) return null; + + class CustomOverlay extends google.maps.OverlayView { + private div: HTMLElement | null = null; + private pos: google.maps.LatLng; + + constructor(pos: google.maps.LatLng) { + super(); + this.pos = pos; + } + + onAdd() { + const div = document.createElement('div'); + div.style.position = 'absolute'; + div.style.cursor = 'pointer'; + div.innerHTML = htmlContent; + this.div = div; + const panes = this.getPanes(); + panes?.overlayMouseTarget.appendChild(div); + } + + draw() { + const overlayProjection = this.getProjection(); + const point = overlayProjection.fromLatLngToDivPixel(this.pos); + if (point && this.div) { + this.div.style.left = (point.x + offset.x) + 'px'; + this.div.style.top = (point.y + offset.y) + 'px'; + } + } + + onRemove() { + if (this.div) { + try { + // Safer element removal + if (this.div.parentNode) { + this.div.parentNode.removeChild(this.div); + } else { + this.div.remove(); + } + } catch (e) { + console.warn('CustomOverlay: element already removed or parent mismatch', e); + } + this.div = null; + } + } + + setPosition(newPos: { lat: number; lng: number }) { + this.pos = new google.maps.LatLng(newPos.lat, newPos.lng); + this.draw(); + } + } + + const overlay = new CustomOverlay(new google.maps.LatLng(position.lat, position.lng)); + overlay.setMap(map.value); + + // Track for cleanup + if (!globalOverlays.has(map.value)) { + globalOverlays.set(map.value, new Set()); + } + globalOverlays.get(map.value)!.add(overlay as any); + + return overlay; + } + + onMounted(() => { + loadMaps() + }) + + return { + map, + isLoaded, + error, + loadMaps, + initMap, + addMarker, + addHtmlMarker, + addNumberedMarker, + addPolyline, + fitBounds, + setCenter, + setZoom, + clearAllOverlays, + } +} + diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts new file mode 100644 index 0000000..f4bbd43 --- /dev/null +++ b/frontend/src/i18n/index.ts @@ -0,0 +1,16 @@ +import { createI18n } from 'vue-i18n' +import es from './locales/es.json' +import en from './locales/en.json' + +const i18n = createI18n({ + legacy: false, + locale: 'es', // Spanish as default + fallbackLocale: 'es', + messages: { + es, + en, + }, +}) + +export default i18n + diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json new file mode 100644 index 0000000..0a6c5d5 --- /dev/null +++ b/frontend/src/i18n/locales/en.json @@ -0,0 +1,144 @@ +{ + "common": { + "loading": "Loading...", + "error": "Error", + "noData": "No data available", + "select": "Select", + "clear": "Clear", + "clearSelection": "Clear selection" + }, + "navigation": { + "map": "Map", + "schedules": "Schedules", + "routes": "Routes", + "favorites": "Favorites", + "taxi": "Taxi", + "coupons": "Offers", + "discover": "Discover", + "profile": "Profile" + }, + "favorites": { + "title": "My Favorites", + "subtitle": "Save your favorite routes, taxis, and businesses for quick access.", + "removeConfirm": "Are you sure you want to remove this favorite?", + "saved": "Saved in favorites", + "contact": "Tap to contact", + "viewDetails": "View details", + "viewSchedules": "Tap to view schedules", + "tabs": { + "routes": "Routes", + "taxis": "Taxis", + "businesses": "Businesses", + "coupons": "Offers" + }, + "empty": { + "routes": "You don't have any saved favorite routes.", + "taxis": "You don't have any saved favorite taxis.", + "businesses": "You don't have any saved favorite businesses.", + "coupons": "You don't have any saved favorite offers." + }, + "cta": { + "routes": "Explore Routes", + "taxis": "View Directory", + "businesses": "Discover Businesses", + "coupons": "View Offers" + } + }, + "header": { + "title": "SIBU", + "switchToLightMode": "Switch to light mode", + "switchToDarkMode": "Switch to dark mode" + }, + "map": { + "title": "Map", + "loadingMap": "Loading map...", + "mapLoadingError": "Map Loading Error", + "commonFixes": "Common fixes:", + "goToConsole": "Go to Google Cloud Console", + "enableMapsApi": "Enable Maps JavaScript API for your project", + "verifyApiKey": "Verify your API key is associated with the project", + "enableBilling": "Ensure billing is enabled (required even for free tier)", + "checkApiRestrictions": "Check API key restrictions allow localhost:5173", + "restartServer": "Restart the dev server after changing .env.development", + "selectRoute": "Select a route", + "route": "Route", + "stops": "stops", + "stop": "stop" + }, + "schedules": { + "title": "Schedules", + "loadingRoutes": "Loading routes...", + "noRoutesAvailable": "No routes available", + "selectRoute": "Select a route", + "route": "Route", + "schedules": "schedules", + "schedule": "schedule", + "departureTime": "Departure time" + }, + "coupons": { + "title": "Offers", + "loadingCoupons": "Loading offers...", + "noCouponsAvailable": "No offers available", + "off": "OFF", + "searchPlaceholder": "Search offers...", + "filterByCategory": "Filter by category", + "apply": "Apply", + "offerDetails": "Offer Details", + "description": "Description", + "validity": "Validity", + "category": "Category", + "viewLocation": "View location", + "validUntil": "Valid until", + "tomorrow": "Tomorrow", + "active": "Active", + "offersCount": "{count} offer | {count} offers" + }, + "taxi": { + "title": "Transport Hub", + "tabLocal": "Local Taxis", + "tabIntercity": "Tourist Trips", + "loadingTaxis": "Loading directory...", + "noTaxisAvailable": "No taxis registered in this area.", + "area": "Zone", + "shift": "Schedule", + "englishSpeakers": "Bilingual drivers", + "callNow": "Call now", + "englishLabel": "ENGLISH", + "allZones": "All zones", + "dayShift": "Day", + "afternoonShift": "Afternoon", + "nightShift": "Night" + }, + "shuttle": { + "title": "Tourist Trips & Shuttles", + "reserve": "Book via WhatsApp", + "perPerson": "per person", + "privateTrip": "private trip", + "duration": "Est. Duration", + "departure": "Departures", + "noShuttles": "No tourist routes available at the moment.", + "filterRoute": "Filter by route", + "allRoutes": "All routes", + "tripType": "Trip type", + "oneWay": "Outbound", + "roundTrip": "Return", + "both": "Both" + }, + "busStop": { + "loadingDetails": "Loading bus stop details...", + "amenities": "Amenities", + "shelter": "Shelter", + "seating": "Seating", + "accessible": "Accessible" + }, + "discover": { + "title": "Discover", + "subtitle": "Explore the best places in Chiriqui", + "filterLabel": "Filter by area:", + "allAreas": "All", + "loading": "Searching for treasures...", + "empty": "No places found in this area yet.", + "exploreMore": "Explore Place", + "tourism": "Tourism" + } +} \ No newline at end of file diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json new file mode 100644 index 0000000..0600133 --- /dev/null +++ b/frontend/src/i18n/locales/es.json @@ -0,0 +1,145 @@ +{ + "common": { + "loading": "Cargando...", + "error": "Error", + "noData": "No hay datos disponibles", + "select": "Seleccionar", + "clear": "Limpiar", + "clearSelection": "Limpiar selección" + }, + "navigation": { + "map": "Mapa", + "schedules": "Horarios", + "routes": "Rutas", + "favorites": "Favoritos", + "taxi": "Transporte", + "coupons": "Ofertas", + "discover": "Descubrir", + "profile": "Perfil" + }, + "favorites": { + "title": "Mis Favoritos", + "subtitle": "Guarda tus rutas, taxis y negocios preferidos para acceder rápido.", + "removeConfirm": "¿Estás seguro de que quieres eliminar este favorito?", + "saved": "Guardado en favoritos", + "contact": "Toca para contactar", + "viewDetails": "Ver detalles", + "viewSchedules": "Toque para ver horarios", + "tabs": { + "routes": "Rutas", + "taxis": "Transporte", + "businesses": "Negocios", + "coupons": "Eventos" + }, + "empty": { + "subtitle": "Aún no tienes favoritos", + "routes": "No tienes rutas favoritas guardadas.", + "taxis": "No tienes taxis favoritos guardados.", + "businesses": "No tienes negocios favoritos guardados.", + "coupons": "No tienes eventos favoritos guardados." + }, + "cta": { + "routes": "Explorar Rutas", + "taxis": "Ver Directorio", + "businesses": "Descubrir Negocios", + "coupons": "Ver Eventos" + } + }, + "header": { + "title": "SIBU", + "switchToLightMode": "Cambiar a modo claro", + "switchToDarkMode": "Cambiar a modo oscuro" + }, + "map": { + "title": "Mapa", + "loadingMap": "Cargando mapa...", + "mapLoadingError": "Error al cargar el mapa", + "commonFixes": "Soluciones comunes:", + "goToConsole": "Ir a Google Cloud Console", + "enableMapsApi": "Habilitar Maps JavaScript API para tu proyecto", + "verifyApiKey": "Verificar que tu clave API esté asociada con el proyecto", + "enableBilling": "Asegurar que la facturación esté habilitada (requerido incluso para el nivel gratuito)", + "checkApiRestrictions": "Verificar que las restricciones de la clave API permitan localhost:5173", + "restartServer": "Reiniciar el servidor de desarrollo después de cambiar .env.development", + "selectRoute": "Seleccionar una ruta", + "route": "Ruta", + "stops": "paradas", + "stop": "parada" + }, + "schedules": { + "title": "Horarios", + "loadingRoutes": "Cargando rutas...", + "noRoutesAvailable": "No hay rutas disponibles", + "selectRoute": "Seleccionar una ruta", + "route": "Ruta", + "schedules": "horarios", + "schedule": "horario", + "departureTime": "Hora de salida" + }, + "coupons": { + "title": "Ofertas", + "loadingCoupons": "Cargando ofertas...", + "noCouponsAvailable": "No hay ofertas disponibles", + "off": "DESCUENTO", + "searchPlaceholder": "Buscar ofertas...", + "filterByCategory": "Filtrar por categoría", + "apply": "Aplicar", + "offerDetails": "Detalles de la Oferta", + "description": "Descripción", + "validity": "Validez", + "category": "Categoría", + "viewLocation": "Ver ubicación", + "validUntil": "Válido hasta", + "tomorrow": "Mañana", + "active": "Activo", + "offersCount": "{count} oferta | {count} ofertas" + }, + "taxi": { + "title": "Centro de Transporte", + "tabLocal": "Taxis Locales", + "tabIntercity": "Viajes Turísticos", + "loadingTaxis": "Cargando directorio...", + "noTaxisAvailable": "No hay taxis registrados en esta zona.", + "area": "Zona", + "shift": "Horario", + "englishSpeakers": "Conductores bilingües", + "callNow": "Llamar ahora", + "englishLabel": "INGLÉS", + "allZones": "Todas las zonas", + "dayShift": "Día", + "afternoonShift": "Tarde", + "nightShift": "Noche" + }, + "shuttle": { + "title": "Viajes Turísticos & Shuttles", + "reserve": "Reservar vía WhatsApp", + "perPerson": "por persona", + "privateTrip": "viaje privado", + "duration": "Duración estimada", + "departure": "Salidas", + "noShuttles": "No hay rutas turísticas disponibles en este momento.", + "filterRoute": "Filtrar por ruta", + "allRoutes": "Todas las rutas", + "tripType": "Tipo de viaje", + "oneWay": "Ida", + "roundTrip": "Vuelta", + "both": "Ambos" + }, + "busStop": { + "loadingDetails": "Cargando detalles de la parada...", + "amenities": "Servicios", + "shelter": "Refugio", + "seating": "Asientos", + "accessible": "Accesible" + }, + "discover": { + "title": "Descubrir", + "subtitle": "Explora los mejores lugares de Chiriquí", + "filterLabel": "Filtrar por área:", + "allAreas": "Todas", + "loading": "Buscando tesoros...", + "empty": "No se encontraron lugares en esta área todavía.", + "exploreMore": "Explorar Lugar", + "tourism": "Turismo" + } +} \ No newline at end of file diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..1ec36cf --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,23 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import router from './router' +import i18n from './i18n' +import './style.css' +import App from './App.vue' + +const app = createApp(App) +const pinia = createPinia() + +app.use(pinia) +app.use(router) +app.use(i18n) + +app.config.errorHandler = (err, _vm, info) => { + console.error('Global Error Handler:', err, info) + // Display error on screen if possible or alert for dev + if (import.meta.env.DEV) { + alert('Frontend Error: ' + err) + } +} + +app.mount('#app') diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..ee57f45 --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,161 @@ +/** Vue Router configuration */ +import { createRouter, createWebHistory } from 'vue-router' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + name: 'splash', + component: () => import('@/views/SplashScreen.vue'), + }, + { + path: '/map', + name: 'map', + component: () => import('@/views/MapView.vue'), + }, + { + path: '/discover', + name: 'discover', + component: () => import('@/views/DiscoverView.vue'), + }, + { + path: '/business/:id', + name: 'business-details', + component: () => import('@/views/BusinessDetailsView.vue'), + }, + { + path: '/routes', + name: 'routes', + component: () => import('@/views/RoutesView.vue'), + }, + { + path: '/schedules', + name: 'schedules', + component: () => import('@/views/SchedulesView.vue'), + }, + { + path: '/coupons', + name: 'coupons', + component: () => import('@/views/CouponsView.vue'), + }, + { + path: '/favorites', + name: 'favorites', + component: () => import('@/views/FavoritesView.vue'), + }, + { + path: '/profile', + name: 'profile', + component: () => import('@/views/ProfileView.vue'), + meta: { requiresAuth: true } + }, + { + path: '/taxi', + name: 'taxi', + component: () => import('@/views/TaxiView.vue'), + }, + { + path: '/bus-stop/:id', + name: 'bus-stop-details', + component: () => import('@/views/BusStopDetailsView.vue'), + }, + { + path: '/login', + name: 'auth', + component: () => import('@/views/AuthView.vue'), + }, + { + path: '/admin', + name: 'admin-panel', + component: () => import('@/views/AdminPanel.vue'), + meta: { requiresAuth: true, role: 'admin' } + }, + { + path: '/admin/bus-stops', + name: 'admin-bus-stops', + component: () => import('@/views/AdminBusStops.vue'), + meta: { requiresAuth: true, role: 'admin' } + }, + { + path: '/admin/routes', + name: 'admin-routes', + component: () => import('@/views/AdminRoutes.vue'), + meta: { requiresAuth: true, role: 'admin' } + }, + { + path: '/admin/reports', + name: 'admin-reports', + component: () => import('@/views/AdminReports.vue'), + meta: { requiresAuth: true, role: 'admin' } + }, + { + path: '/admin/schedules', + name: 'admin-schedules', + component: () => import('@/views/AdminSchedules.vue'), + meta: { requiresAuth: true, role: 'admin' } + }, + { + path: '/admin/drivers', + name: 'admin-drivers', + component: () => import('@/views/AdminDrivers.vue'), + meta: { requiresAuth: true, role: 'admin' } + }, + { + path: '/admin/analytics', + name: 'admin-analytics', + component: () => import('@/views/StrategicAnalytics.vue'), + meta: { requiresAuth: true, role: 'admin' } + }, + { + path: '/admin/taxis', + name: 'admin-taxis', + component: () => import('@/views/AdminTaxis.vue'), + meta: { requiresAuth: true, role: 'admin' } + }, + { + path: '/admin/shuttles', + name: 'admin-shuttles', + component: () => import('@/views/AdminShuttles.vue'), + meta: { requiresAuth: true, role: 'admin' } + }, + { + path: '/promoter', + name: 'promoter-dashboard', + component: () => import('@/views/PromoterDashboard.vue'), + meta: { requiresAuth: true, role: ['PROMOTER', 'ADMIN'] } + }, + { + path: '/driver', + name: 'driver-dashboard', + component: () => import('@/views/DriverDashboard.vue'), + meta: { requiresAuth: true, role: ['DRIVER', 'ADMIN'] } + }, + ], +}) + +router.beforeEach((to, _from, next) => { + const token = localStorage.getItem('auth_token') + const role = localStorage.getItem('user_role')?.toUpperCase() + + if (to.meta.requiresAuth && !token) { + next('/login') + } else if (to.meta.role) { + const allowedRoles = Array.isArray(to.meta.role) ? to.meta.role : [to.meta.role] + const hasAccess = allowedRoles.some(r => r.toUpperCase() === role) + + if (!hasAccess) { + if (role === 'ADMIN') next('/admin') + else if (role === 'DRIVER') next('/driver') + else if (role === 'PROMOTER') next('/promoter') + else next('/map') + } else { + next() + } + } else { + next() + } +}) + +export default router + diff --git a/frontend/src/services/analyticsService.ts b/frontend/src/services/analyticsService.ts new file mode 100644 index 0000000..9f14bfd --- /dev/null +++ b/frontend/src/services/analyticsService.ts @@ -0,0 +1,22 @@ +import { apiClient } from './apiClient' + +export interface AnalyticsEvent { + event_name: 'app_open' | 'screen_view' | 'route_selected' | 'stop_selected' | 'schedule_viewed' | 'reminder_created' | 'promo_view' | 'promo_click' | 'taxi_view' | 'taxi_click' | 'shuttle_view' | 'shuttle_contact' | 'business_view' | 'business_contact' + screen_name?: string + item_id?: string + properties?: Record +} + +export const analyticsService = { + logEvent(event: AnalyticsEvent) { + // Log asynchronously without awaiting to avoid blocking UI + apiClient.post('/api/analytics/event', event).catch(error => { + console.warn('Analytics capture failed:', error) + }) + }, + + async getStats() { + const response = await apiClient.get('/api/analytics/dashboard/stats') + return response.data + } +} diff --git a/frontend/src/services/apiClient.ts b/frontend/src/services/apiClient.ts new file mode 100644 index 0000000..9e70301 --- /dev/null +++ b/frontend/src/services/apiClient.ts @@ -0,0 +1,59 @@ +/** Base API client for making HTTP requests to the backend */ +import axios from 'axios' +import type { AxiosInstance, AxiosError } from 'axios' + +export const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000' + +class ApiClient { + private client: AxiosInstance + + constructor() { + this.client = axios.create({ + baseURL: API_URL, + headers: { + 'Content-Type': 'application/json', + }, + timeout: 10000, + }) + + // Request interceptor + this.client.interceptors.request.use( + (config) => { + const token = localStorage.getItem('auth_token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error) => { + return Promise.reject(error) + } + ) + + // Response interceptor + this.client.interceptors.response.use( + (response) => response, + (error: AxiosError) => { + // Handle common errors + if (error.response) { + // Server responded with error status + console.error('API Error:', error.response.status, error.response.data) + } else if (error.request) { + // Request made but no response + console.error('Network Error:', error.request) + } else { + // Something else happened + console.error('Error:', error.message) + } + return Promise.reject(error) + } + ) + } + + get instance(): AxiosInstance { + return this.client + } +} + +export const apiClient = new ApiClient().instance + diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts new file mode 100644 index 0000000..5f981b9 --- /dev/null +++ b/frontend/src/services/authService.ts @@ -0,0 +1,55 @@ +import { apiClient, API_URL } from './apiClient' + +export interface LoginResponse { + access_token: string + token_type: string + role: string + full_name: string + profile_photo_url?: string +} + +export const authService = { + async login(params: { email: string; password: string; keep_session?: boolean }): Promise { + const response = await apiClient.post('/api/auth/login', params) + return response.data + }, + + async registerPassenger(data: any) { + const response = await apiClient.post('/api/auth/register/passenger', data) + return response.data + }, + + async registerDriver(formData: FormData) { + const response = await apiClient.post('/api/auth/register/driver', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + return response.data + }, + + async getCurrentUser() { + const response = await apiClient.get('/api/auth/me') + return response.data + }, + + async updateMe(formData: FormData) { + const response = await apiClient.patch('/api/auth/me', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + return response.data + }, + + logout() { + localStorage.removeItem('auth_token') + localStorage.removeItem('user_role') + localStorage.removeItem('user_name') + localStorage.removeItem('profile_photo_url') + }, + + getApiUrl() { + return API_URL + } +} diff --git a/frontend/src/services/busStopsService.ts b/frontend/src/services/busStopsService.ts new file mode 100644 index 0000000..d184abe --- /dev/null +++ b/frontend/src/services/busStopsService.ts @@ -0,0 +1,57 @@ +/** Service for bus stop-related API calls */ +import { apiClient } from './apiClient' +import type { BusStop, Route } from '@/types' + +export const busStopsService = { + /** Get all bus stops */ + async getAllBusStops(): Promise { + const response = await apiClient.get('/api/bus-stops') + return response.data + }, + + /** Get a single bus stop by ID */ + async getBusStopById(id: string): Promise { + const response = await apiClient.get(`/api/bus-stops/${id}`) + return response.data + }, + + /** Get all routes passing through a bus stop */ + async getBusStopRoutes(stopId: string): Promise { + const response = await apiClient.get(`/api/bus-stops/${stopId}/routes`) + return response.data + }, + + /** Get estimated next bus arrivals (Mock Data) */ + async getNextBusArrivals(_stopId: string): Promise<{ routeName: string; arrivalTime: string }[]> { + // Mock delay to simulate network request + await new Promise(resolve => setTimeout(resolve, 500)); + + // Generate some random mock arrivals + const mockArrivals = [ + { routeName: "Ruta Boquete - David", arrivalTime: "5 min" }, + { routeName: "Ruta David - Boquete", arrivalTime: "12 min" }, + { routeName: "Ruta Circular", arrivalTime: "25 min" } + ]; + + // Randomly return 1-3 arrivals + return mockArrivals.slice(0, Math.floor(Math.random() * 3) + 1); + }, + + /** Create a new bus stop (Admin) */ + async createBusStop(data: import('@/types').BusStopCreate): Promise { + const response = await apiClient.post('/api/bus-stops', data) + return response.data + }, + + /** Update a bus stop (Admin) */ + async updateBusStop(id: string, data: import('@/types').BusStopUpdate): Promise { + const response = await apiClient.put(`/api/bus-stops/${id}`, data) + return response.data + }, + + /** Delete a bus stop (Admin) */ + async deleteBusStop(id: string): Promise { + await apiClient.delete(`/api/bus-stops/${id}`) + } +} + diff --git a/frontend/src/services/businessService.ts b/frontend/src/services/businessService.ts new file mode 100644 index 0000000..78eb829 --- /dev/null +++ b/frontend/src/services/businessService.ts @@ -0,0 +1,42 @@ +/** Service for business-related API calls */ +import { apiClient } from './apiClient' +import type { Business } from '@/types' + +export const businessService = { + /** Get all businesses */ + async getAllBusinesses(): Promise { + const response = await apiClient.get('/api/businesses') + return response.data + }, + + /** Get a single business by ID */ + async getBusiness(id: string): Promise { + const response = await apiClient.get(`/api/businesses/${id}`) + return response.data + }, + + /** Create a new business */ + async createBusiness(businessData: FormData): Promise { + const response = await apiClient.post('/api/businesses', businessData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + return response.data + }, + + /** Update an existing business */ + async updateBusiness(id: string, businessData: FormData): Promise { + const response = await apiClient.patch(`/api/businesses/${id}`, businessData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + return response.data + }, + + /** Delete a business */ + async deleteBusiness(id: string): Promise { + await apiClient.delete(`/api/businesses/${id}`) + }, +} diff --git a/frontend/src/services/couponsService.ts b/frontend/src/services/couponsService.ts new file mode 100644 index 0000000..ec75902 --- /dev/null +++ b/frontend/src/services/couponsService.ts @@ -0,0 +1,61 @@ +/** Service for coupon-related API calls */ +import { apiClient } from './apiClient' +import type { Coupon } from '@/types' + +export interface CouponFilters { + category?: string + is_active?: boolean + active_only?: boolean +} + +export const couponsService = { + /** Get all coupons with optional filters */ + async getAllCoupons(filters?: CouponFilters): Promise { + const response = await apiClient.get('/api/coupons', { + params: filters, + }) + return response.data + }, + + /** Get a single coupon by ID */ + async getCouponById(id: string): Promise { + const response = await apiClient.get(`/api/coupons/${id}`) + return response.data + }, + + /** Create a new coupon */ + async createCoupon(coupon: Omit): Promise { + const response = await apiClient.post('/api/coupons', coupon) + return response.data + }, + + /** Update an existing coupon */ + async updateCoupon(id: string, coupon: Partial): Promise { + const response = await apiClient.patch(`/api/coupons/${id}`, coupon) + return response.data + }, + + /** Delete a coupon */ + async deleteCoupon(id: string): Promise { + await apiClient.delete(`/api/coupons/${id}`) + }, + + /** Claim a coupon */ + async claimCoupon(id: string): Promise { + const response = await apiClient.post(`/api/coupons/${id}/claim`) + return response.data + }, + + /** Get current user's claimed coupons */ + async getMyCoupons(): Promise { + const response = await apiClient.get('/api/coupons/my-coupons') + return response.data + }, + + /** Validate a coupon by code (merchants/drivers only) */ + async validateCoupon(code: string): Promise { + const response = await apiClient.post(`/api/coupons/validate/${code}`) + return response.data + } +} + diff --git a/frontend/src/services/favoritesService.ts b/frontend/src/services/favoritesService.ts new file mode 100644 index 0000000..c1685be --- /dev/null +++ b/frontend/src/services/favoritesService.ts @@ -0,0 +1,74 @@ +/** Service for favorite-related API calls */ +import { apiClient } from './apiClient' +import type { Favorite } from '@/types' + +export const favoritesService = { + /** Get all favorites for the current user */ + async getMyFavorites(itemType?: string): Promise { + const params = itemType ? { item_type: itemType } : {} + const response = await apiClient.get('/api/favorites', { params }) + return response.data + }, + + /** Add a new favorite */ + async addFavorite( + itemType: 'route' | 'stop' | 'taxi' | 'coupon' | 'business', + itemId: string, + itemName?: string, + itemImage?: string + ): Promise { + const response = await apiClient.post('/api/favorites', { + item_type: itemType, + item_id: itemId, + item_name: itemName, + item_image: itemImage + }) + return response.data + }, + + /** Remove a favorite by type and ID */ + async removeFavorite(itemType: string, itemId: string): Promise { + await apiClient.delete(`/api/favorites/${itemType}/${itemId}`) + }, + + /** Remove a favorite by favorite ID (legacy support) */ + async removeFavoriteById(favoriteId: string): Promise { + // This requires finding the favorite first to get type and id + const favorites = await this.getMyFavorites() + const favorite = favorites.find(f => f.id === favoriteId) + if (favorite) { + await this.removeFavorite(favorite.item_type, favorite.item_id) + } + }, + + /** Check if an item is favorited */ + async checkFavorite(itemType: string, itemId: string): Promise { + try { + const response = await apiClient.get<{ is_favorite: boolean }>( + `/api/favorites/check/${itemType}/${itemId}` + ) + return response.data.is_favorite + } catch (error) { + console.error('Error checking favorite:', error) + return false + } + }, + + /** Toggle favorite status */ + async toggleFavorite( + itemType: 'route' | 'stop' | 'taxi' | 'coupon' | 'business', + itemId: string, + itemName?: string, + itemImage?: string + ): Promise { + const isFavorite = await this.checkFavorite(itemType, itemId) + + if (isFavorite) { + await this.removeFavorite(itemType, itemId) + return false + } else { + await this.addFavorite(itemType, itemId, itemName, itemImage) + return true + } + } +} diff --git a/frontend/src/services/reportsService.ts b/frontend/src/services/reportsService.ts new file mode 100644 index 0000000..4204496 --- /dev/null +++ b/frontend/src/services/reportsService.ts @@ -0,0 +1,28 @@ +import { apiClient } from './apiClient'; + +export interface Report { + id: string; + user_id?: string; + user_name?: string; + message: string; + status: 'pending' | 'resolved' | 'archived'; + created_at: string; +} + +export const reportsService = { + async sendReport(message: string) { + const response = await apiClient.post('/api/reports', { message }); + return response.data; + }, + + async getReports() { + // This would be for the admin + const response = await apiClient.get('/api/reports'); + return response.data; + }, + + async updateReportStatus(reportId: string, status: string) { + const response = await apiClient.patch(`/api/reports/${reportId}`, { status }); + return response.data; + } +}; diff --git a/frontend/src/services/routesService.ts b/frontend/src/services/routesService.ts new file mode 100644 index 0000000..10b70cd --- /dev/null +++ b/frontend/src/services/routesService.ts @@ -0,0 +1,56 @@ +/** Service for route-related API calls */ +import { apiClient } from './apiClient' +import type { Route, BusStop } from '@/types' + +export const routesService = { + /** Get all routes with optional filtering */ + async getAllRoutes(filters?: { originCity?: string, destinationCity?: string }): Promise { + const response = await apiClient.get('/api/routes', { + params: { + origin_city: filters?.originCity, + destination_city: filters?.destinationCity + } + }) + return response.data + }, + + /** Get a single route by ID */ + async getRouteById(id: string): Promise { + const response = await apiClient.get(`/api/routes/${id}`) + return response.data + }, + + /** Get all stops for a route */ + async getRouteStops(routeId: string): Promise { + const response = await apiClient.get(`/api/routes/${routeId}/stops`) + return response.data + }, + + /** Create a new route (Admin) */ + async createRoute(data: import('@/types').RouteCreate): Promise { + const response = await apiClient.post('/api/routes', data) + return response.data + }, + + /** Update a route (Admin) */ + async updateRoute(id: string, data: import('@/types').RouteUpdate): Promise { + const response = await apiClient.put(`/api/routes/${id}`, data) + return response.data + }, + + /** Delete a route (Admin) */ + async deleteRoute(id: string): Promise { + await apiClient.delete(`/api/routes/${id}`) + }, + + /** Add a stop to a route (Admin) */ + async addStopToRoute(routeId: string, data: import('@/types').RouteStopCreate): Promise { + await apiClient.post(`/api/routes/${routeId}/stops`, data) + }, + + /** Update a stop on a route (Admin) - including reorder */ + async updateRouteStop(routeId: string, stopId: string, data: import('@/types').RouteStopUpdate): Promise { + await apiClient.put(`/api/routes/${routeId}/stops/${stopId}`, data) + } +} + diff --git a/frontend/src/services/schedulesService.ts b/frontend/src/services/schedulesService.ts new file mode 100644 index 0000000..8531090 --- /dev/null +++ b/frontend/src/services/schedulesService.ts @@ -0,0 +1,32 @@ +import { apiClient } from './apiClient'; + +export const schedulesService = { + async getRouteSchedules(routeId: string, onlyPublished = true) { + const response = await apiClient.get('/api/schedules', { + params: { route_id: routeId, only_published: onlyPublished } + }); + return response.data; + }, + + async getStopSchedules(stopId: string, onlyPublished = true) { + const response = await apiClient.get('/api/schedules', { + params: { stop_id: stopId, only_published: onlyPublished } + }); + return response.data; + }, + + async createSchedule(scheduleData: any) { + const response = await apiClient.post('/api/schedules', scheduleData); + return response.data; + }, + + async updateSchedule(scheduleId: string, updateData: any) { + const response = await apiClient.put(`/api/schedules/${scheduleId}`, updateData); + return response.data; + }, + + async deleteSchedule(scheduleId: string) { + const response = await apiClient.delete(`/api/schedules/${scheduleId}`); + return response.data; + } +}; diff --git a/frontend/src/services/shuttlesService.ts b/frontend/src/services/shuttlesService.ts new file mode 100644 index 0000000..66dab12 --- /dev/null +++ b/frontend/src/services/shuttlesService.ts @@ -0,0 +1,27 @@ +/** Service for shuttle-related API calls (Intercity/Tourism) */ +import { apiClient } from './apiClient' +import type { Shuttle } from '@/types' + +export interface ShuttleFilters { + origin?: string + destination?: string + company_name?: string + trip_type?: string + is_active?: boolean +} + +export const shuttlesService = { + /** Get all shuttles with optional filters */ + async getAllShuttles(filters?: ShuttleFilters): Promise { + const response = await apiClient.get('/api/shuttles', { + params: filters, + }) + return response.data + }, + + /** Get a single shuttle by ID */ + async getShuttleById(id: string): Promise { + const response = await apiClient.get(`/api/shuttles/${id}`) + return response.data + }, +} diff --git a/frontend/src/services/taxisService.ts b/frontend/src/services/taxisService.ts new file mode 100644 index 0000000..5f12c86 --- /dev/null +++ b/frontend/src/services/taxisService.ts @@ -0,0 +1,27 @@ +/** Service for taxi-related API calls */ +import { apiClient } from './apiClient' +import type { Taxi } from '@/types' + +export interface TaxiFilters { + corregimiento?: string + shift?: string + english_speaking?: boolean + is_active?: boolean +} + +export const taxisService = { + /** Get all taxis with optional filters */ + async getAllTaxis(filters?: TaxiFilters): Promise { + const response = await apiClient.get('/api/taxis', { + params: filters, + }) + return response.data + }, + + /** Get a single taxi by ID */ + async getTaxiById(id: string): Promise { + const response = await apiClient.get(`/api/taxis/${id}`) + return response.data + }, +} + diff --git a/frontend/src/services/telemetryService.ts b/frontend/src/services/telemetryService.ts new file mode 100644 index 0000000..ff2c652 --- /dev/null +++ b/frontend/src/services/telemetryService.ts @@ -0,0 +1,32 @@ +import { apiClient } from './apiClient' + +export interface TelemetryData { + latitude: number + longitude: number + speed?: number + heading?: number + status?: 'active' | 'offline' | 'break' +} + +export interface ActiveUnit { + user_id: string + full_name: string + latitude: number + longitude: number + speed?: number + heading?: number + timestamp: string + vehicle_type: string + license_plate: string +} + +export const telemetryService = { + async sendTelemetry(data: TelemetryData) { + return await apiClient.post('/api/telemetry', data) + }, + + async getActiveUnits(): Promise { + const response = await apiClient.get('/api/telemetry/active') + return response.data + } +} diff --git a/frontend/src/services/usersService.ts b/frontend/src/services/usersService.ts new file mode 100644 index 0000000..8da9c67 --- /dev/null +++ b/frontend/src/services/usersService.ts @@ -0,0 +1,27 @@ +import { apiClient } from './apiClient'; + +export const usersService = { + async searchUsers(email: string) { + const response = await apiClient.get('/api/users/search', { + params: { email } + }); + return response.data; + }, + + async getUserDetails(userId: string) { + const response = await apiClient.get(`/api/users/${userId}`); + return response.data; + }, + + async getPendingDrivers() { + const response = await apiClient.get('/api/users/pending-drivers'); + return response.data; + }, + + async verifyUser(userId: string, isVerified: boolean) { + const response = await apiClient.post(`/api/users/${userId}/verify`, null, { + params: { is_verified: isVerified } + }); + return response.data; + } +}; diff --git a/frontend/src/shims-vue.d.ts b/frontend/src/shims-vue.d.ts new file mode 100644 index 0000000..1a9fbc3 --- /dev/null +++ b/frontend/src/shims-vue.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts new file mode 100644 index 0000000..8ee02fa --- /dev/null +++ b/frontend/src/stores/auth.ts @@ -0,0 +1,47 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' + + +export const useAuthStore = defineStore('auth', () => { + const token = ref(localStorage.getItem('auth_token')) + const role = ref(localStorage.getItem('user_role')) + const userName = ref(localStorage.getItem('user_name')) + + const isAuthenticated = computed(() => !!token.value) + const isAdmin = computed(() => role.value?.toUpperCase() === 'ADMIN') + const isDriver = computed(() => role.value?.toUpperCase() === 'DRIVER') + const isPromoter = computed(() => role.value?.toUpperCase() === 'PROMOTER') + const isPassenger = computed(() => !role.value || role.value?.toUpperCase() === 'PASSENGER') + + function login(newToken: string, newRole: string, newName: string) { + token.value = newToken + role.value = newRole + userName.value = newName + localStorage.setItem('auth_token', newToken) + localStorage.setItem('user_role', newRole) + localStorage.setItem('user_name', newName) + } + + function logout() { + token.value = null + role.value = null + userName.value = null + localStorage.removeItem('auth_token') + localStorage.removeItem('user_role') + localStorage.removeItem('user_name') + window.location.href = '/' + } + + return { + token, + role, + userName, + isAuthenticated, + isAdmin, + isDriver, + isPromoter, + isPassenger, + login, + logout + } +}) diff --git a/frontend/src/stores/busStop.ts b/frontend/src/stores/busStop.ts new file mode 100644 index 0000000..4e72b22 --- /dev/null +++ b/frontend/src/stores/busStop.ts @@ -0,0 +1,57 @@ +/** Pinia store for bus stop management */ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import type { BusStop } from '@/types' +import { busStopsService } from '@/services/busStopsService' + +export const useBusStopStore = defineStore('busStop', () => { + const selectedStop = ref(null) + const busStops = ref([]) + const isLoading = ref(false) + const error = ref(null) + + async function loadBusStops(force = false) { + if (!force && busStops.value.length > 0) { + return + } + + isLoading.value = true + error.value = null + try { + busStops.value = await busStopsService.getAllBusStops() + } catch (e) { + error.value = e instanceof Error ? e.message : 'Failed to load bus stops' + console.error('Error loading bus stops:', e) + } finally { + isLoading.value = false + } + } + + async function loadBusStopById(id: string) { + isLoading.value = true + error.value = null + try { + selectedStop.value = await busStopsService.getBusStopById(id) + } catch (e) { + error.value = e instanceof Error ? e.message : 'Failed to load bus stop' + console.error('Error loading bus stop:', e) + } finally { + isLoading.value = false + } + } + + function setSelectedStop(stop: BusStop | null) { + selectedStop.value = stop + } + + return { + selectedStop, + busStops, + isLoading, + error, + loadBusStops, + loadBusStopById, + setSelectedStop, + } +}) + diff --git a/frontend/src/stores/coupon.ts b/frontend/src/stores/coupon.ts new file mode 100644 index 0000000..2a6e9d4 --- /dev/null +++ b/frontend/src/stores/coupon.ts @@ -0,0 +1,65 @@ +/** Pinia store for coupon management */ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import type { Coupon } from '@/types' +import { couponsService, type CouponFilters } from '@/services/couponsService' + +export const useCouponStore = defineStore('coupon', () => { + const coupons = ref([]) + const isLoading = ref(false) + const error = ref(null) + const myCoupons = ref([]) + const filters = ref({}) + + async function loadCoupons(newFilters?: CouponFilters) { + isLoading.value = true + error.value = null + if (newFilters) { + filters.value = newFilters + } + try { + coupons.value = await couponsService.getAllCoupons(filters.value) + } catch (e) { + error.value = e instanceof Error ? e.message : 'Failed to load coupons' + console.error('Error loading coupons:', e) + } finally { + isLoading.value = false + } + } + + async function loadMyCoupons() { + try { + myCoupons.value = await couponsService.getMyCoupons() + } catch (e) { + console.error('Error loading my coupons:', e) + } + } + + async function claimCoupon(id: string) { + try { + await couponsService.claimCoupon(id) + await loadMyCoupons() + return true + } catch (e: any) { + const msg = e.response?.data?.detail || e.message + throw new Error(msg) + } + } + + function setFilters(newFilters: CouponFilters) { + filters.value = newFilters + loadCoupons() + } + + return { + coupons, + myCoupons, + isLoading, + error, + filters, + loadCoupons, + loadMyCoupons, + claimCoupon, + setFilters, + } +}) diff --git a/frontend/src/stores/favorites.ts b/frontend/src/stores/favorites.ts new file mode 100644 index 0000000..3f2b576 --- /dev/null +++ b/frontend/src/stores/favorites.ts @@ -0,0 +1,114 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { apiClient } from '@/services/apiClient' + +export interface Favorite { + id: string + user_id: string + item_type: 'coupon' | 'business' | 'taxi' | 'route' + item_id: string + item_name?: string + item_image?: string + created_at: string +} + +export const useFavoritesStore = defineStore('favorites', () => { + const favorites = ref([]) + const isLoading = ref(false) + + // Computed + const coupons = computed(() => favorites.value.filter(f => f.item_type === 'coupon')) + const businesses = computed(() => favorites.value.filter(f => f.item_type === 'business')) + const taxis = computed(() => favorites.value.filter(f => f.item_type === 'taxi')) + const routes = computed(() => favorites.value.filter(f => f.item_type === 'route')) + + // Actions + async function loadFavorites() { + isLoading.value = true + try { + const response = await apiClient.get('/api/favorites') + favorites.value = response.data + } catch (error) { + console.error('Error loading favorites:', error) + } finally { + isLoading.value = false + } + } + + async function addFavorite( + itemType: 'coupon' | 'business' | 'taxi' | 'route', + itemId: string, + itemName?: string, + itemImage?: string + ) { + try { + const response = await apiClient.post('/api/favorites', { + item_type: itemType, + item_id: itemId, + item_name: itemName, + item_image: itemImage + }) + favorites.value.unshift(response.data) + return true + } catch (error: any) { + if (error.response?.status === 400) { + // Already favorited + return false + } + console.error('Error adding favorite:', error) + throw error + } + } + + async function removeFavorite(itemType: string, itemId: string) { + try { + await apiClient.delete(`/api/favorites/${itemType}/${itemId}`) + favorites.value = favorites.value.filter( + f => !(f.item_type === itemType && f.item_id === itemId) + ) + return true + } catch (error) { + console.error('Error removing favorite:', error) + throw error + } + } + + async function toggleFavorite( + itemType: 'coupon' | 'business' | 'taxi' | 'route', + itemId: string, + itemName?: string, + itemImage?: string + ) { + const existing = favorites.value.find( + f => f.item_type === itemType && f.item_id === itemId + ) + + if (existing) { + await removeFavorite(itemType, itemId) + return false + } else { + await addFavorite(itemType, itemId, itemName, itemImage) + return true + } + } + + function isFavorite(itemType: string, itemId: string): boolean { + return favorites.value.some( + f => f.item_type === itemType && f.item_id === itemId + ) + } + + return { + favorites, + isLoading, + coupons, + businesses, + taxis, + routes, + loadFavorites, + addFavorite, + removeFavorite, + toggleFavorite, + isFavorite + } +}) diff --git a/frontend/src/stores/map.ts b/frontend/src/stores/map.ts new file mode 100644 index 0000000..5dc0bea --- /dev/null +++ b/frontend/src/stores/map.ts @@ -0,0 +1,39 @@ +/** Pinia store for map state */ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import type { BusStop } from '@/types' + +export const useMapStore = defineStore('map', () => { + const markers = ref([]) + const selectedStop = ref(null) + const center = ref({ lat: 8.4177, lng: -82.4270 }) // Panama coordinates (David/Boquete area) + const zoom = ref(12) + + function setMarkers(stops: BusStop[]) { + markers.value = stops + } + + function setSelectedStop(stop: BusStop | null) { + selectedStop.value = stop + } + + function setCenter(lat: number, lng: number) { + center.value = { lat, lng } + } + + function setZoom(level: number) { + zoom.value = level + } + + return { + markers, + selectedStop, + center, + zoom, + setMarkers, + setSelectedStop, + setCenter, + setZoom, + } +}) + diff --git a/frontend/src/stores/route.ts b/frontend/src/stores/route.ts new file mode 100644 index 0000000..4e7fb0a --- /dev/null +++ b/frontend/src/stores/route.ts @@ -0,0 +1,78 @@ +/** Pinia store for route management */ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import type { Route, BusStop } from '@/types' +import { routesService } from '@/services/routesService' + +export const useRouteStore = defineStore('route', () => { + const selectedRouteId = ref(null) + const selectedRouteName = ref(null) + const selectedRouteStops = ref([]) + const allRoutes = ref([]) + const isLoadingRoutes = ref(false) + const isLoadingStops = ref(false) + const error = ref(null) + + const hasSelectedRoute = computed(() => selectedRouteId.value !== null && selectedRouteName.value !== null) + + async function loadRoutes(filters?: { originCity?: string, destinationCity?: string }, force = false) { + if (!force && !filters && allRoutes.value.length > 0) { + return + } + + isLoadingRoutes.value = true + error.value = null + try { + allRoutes.value = await routesService.getAllRoutes(filters) + } catch (e) { + error.value = e instanceof Error ? e.message : 'Failed to load routes' + console.error('Error loading routes:', e) + } finally { + isLoadingRoutes.value = false + } + } + + async function loadRouteStops(routeId: string) { + isLoadingStops.value = true + error.value = null + try { + selectedRouteStops.value = await routesService.getRouteStops(routeId) + } catch (e) { + error.value = e instanceof Error ? e.message : 'Failed to load route stops' + console.error('Error loading route stops:', e) + selectedRouteStops.value = [] + } finally { + isLoadingStops.value = false + } + } + + async function selectRoute(routeId: string, routeName: string) { + if (selectedRouteId.value === routeId) return + selectedRouteId.value = routeId + selectedRouteName.value = routeName + selectedRouteStops.value = [] // Clear old stops immediately + await loadRouteStops(routeId) + } + + function clearSelection() { + selectedRouteId.value = null + selectedRouteName.value = null + selectedRouteStops.value = [] + } + + return { + selectedRouteId, + selectedRouteName, + selectedRouteStops, + allRoutes, + isLoadingRoutes, + isLoadingStops, + error, + hasSelectedRoute, + loadRoutes, + loadRouteStops, + selectRoute, + clearSelection, + } +}) + diff --git a/frontend/src/stores/schedule.ts b/frontend/src/stores/schedule.ts new file mode 100644 index 0000000..b4f1705 --- /dev/null +++ b/frontend/src/stores/schedule.ts @@ -0,0 +1,46 @@ +/** Pinia store for schedule management */ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import type { BusSchedule } from '@/types' +import { schedulesService } from '@/services/schedulesService' + +export const useScheduleStore = defineStore('schedule', () => { + const schedules = ref([]) + const isLoading = ref(false) + const error = ref(null) + + async function loadRouteSchedules(routeId: string) { + isLoading.value = true + error.value = null + try { + schedules.value = await schedulesService.getRouteSchedules(routeId) + } catch (e) { + error.value = e instanceof Error ? e.message : 'Failed to load schedules' + console.error('Error loading schedules:', e) + } finally { + isLoading.value = false + } + } + + async function loadStopSchedules(stopId: string) { + isLoading.value = true + error.value = null + try { + schedules.value = await schedulesService.getStopSchedules(stopId) + } catch (e) { + error.value = e instanceof Error ? e.message : 'Failed to load schedules' + console.error('Error loading schedules:', e) + } finally { + isLoading.value = false + } + } + + return { + schedules, + isLoading, + error, + loadRouteSchedules, + loadStopSchedules, + } +}) + diff --git a/frontend/src/stores/shuttle.ts b/frontend/src/stores/shuttle.ts new file mode 100644 index 0000000..3077b7a --- /dev/null +++ b/frontend/src/stores/shuttle.ts @@ -0,0 +1,36 @@ +/** Pinia store for shuttle management (Intercity/Tourism) */ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import type { Shuttle } from '@/types' +import { shuttlesService, type ShuttleFilters } from '@/services/shuttlesService' + +export const useShuttleStore = defineStore('shuttle', () => { + const shuttles = ref([]) + const isLoading = ref(false) + const error = ref(null) + const filters = ref({}) + + async function loadShuttles(newFilters?: ShuttleFilters) { + isLoading.value = true + error.value = null + if (newFilters) { + filters.value = newFilters + } + try { + shuttles.value = await shuttlesService.getAllShuttles(filters.value) + } catch (e) { + error.value = e instanceof Error ? e.message : 'Failed to load shuttles' + console.error('Error loading shuttles:', e) + } finally { + isLoading.value = false + } + } + + return { + shuttles, + isLoading, + error, + filters, + loadShuttles, + } +}) diff --git a/frontend/src/stores/taxi.ts b/frontend/src/stores/taxi.ts new file mode 100644 index 0000000..3977ca3 --- /dev/null +++ b/frontend/src/stores/taxi.ts @@ -0,0 +1,43 @@ +/** Pinia store for taxi management */ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import type { Taxi } from '@/types' +import { taxisService, type TaxiFilters } from '@/services/taxisService' + +export const useTaxiStore = defineStore('taxi', () => { + const taxis = ref([]) + const isLoading = ref(false) + const error = ref(null) + const filters = ref({}) + + async function loadTaxis(newFilters?: TaxiFilters) { + isLoading.value = true + error.value = null + if (newFilters) { + filters.value = newFilters + } + try { + taxis.value = await taxisService.getAllTaxis(filters.value) + } catch (e) { + error.value = e instanceof Error ? e.message : 'Failed to load taxis' + console.error('Error loading taxis:', e) + } finally { + isLoading.value = false + } + } + + function setFilters(newFilters: TaxiFilters) { + filters.value = newFilters + loadTaxis() + } + + return { + taxis, + isLoading, + error, + filters, + loadTaxis, + setFilters, + } +}) + diff --git a/frontend/src/stores/theme.ts b/frontend/src/stores/theme.ts new file mode 100644 index 0000000..ce43c06 --- /dev/null +++ b/frontend/src/stores/theme.ts @@ -0,0 +1,60 @@ +/** Pinia store for theme management */ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export const useThemeStore = defineStore('theme', () => { + // Check localStorage first, then system preference + const getInitialTheme = (): boolean => { + const stored = localStorage.getItem('darkMode') + if (stored !== null) { + return stored === 'true' + } + // Check system preference + return window.matchMedia('(prefers-color-scheme: dark)').matches + } + + const isDarkMode = ref(getInitialTheme()) + + // Apply theme to document + function applyTheme() { + if (isDarkMode.value) { + document.documentElement.classList.add('dark') + document.documentElement.classList.remove('light-theme') + } else { + document.documentElement.classList.remove('dark') + document.documentElement.classList.add('light-theme') + } + localStorage.setItem('darkMode', String(isDarkMode.value)) + } + + function toggleDarkMode() { + isDarkMode.value = !isDarkMode.value + applyTheme() + } + + function setDarkMode(value: boolean) { + isDarkMode.value = value + applyTheme() + } + + // Watch for system theme changes + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + const handleSystemThemeChange = (e: MediaQueryListEvent) => { + if (!localStorage.getItem('darkMode')) { + isDarkMode.value = e.matches + applyTheme() + } + } + mediaQuery.addEventListener('change', handleSystemThemeChange) + + // Apply theme on initialization + applyTheme() + + return { + isDarkMode, + toggleDarkMode, + setDarkMode, + applyTheme, + } +}) + diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..61fd53c --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,5 @@ +/* Global styles are now managed in App.vue and component-specific styles. + This file has been cleared to prevent layout conflicts. */ +* { + box-sizing: border-box; +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..7d1ccb8 --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,186 @@ +/** Type definitions for the SIBU transportation app */ + +export type RouteStatus = 'active' | 'inactive' | 'maintenance' +export type StopType = 'terminal' | 'regular' | 'express_only' +export type ScheduleType = 'weekday' | 'weekend' | 'holiday' + +export interface Route { + id: string + name: string + description?: string + color: string + direction: string + origin_city?: string + destination_city?: string + distance_km?: number + estimated_duration_minutes?: number + average_speed_kmh?: number + status: RouteStatus + created_at?: string + updated_at?: string +} + +export interface BusStop { + id: string + name: string + latitude: number + longitude: number + city?: string + address?: string + parent_id?: string + side?: string + stop_type: StopType + has_shelter: boolean + has_seating: boolean + is_accessible: boolean + created_at?: string + updated_at?: string + // Route-specific fields (from route_stops junction table) OR global order + stop_order?: number + travel_time_minutes?: number + stop_delay_minutes?: number + is_pickup_point?: boolean + is_dropoff_point?: boolean +} + +export interface RouteStop { + id: string + route_id: string + stop_id: string + stop_order: number + travel_time_minutes?: number + stop_delay_minutes?: number + is_pickup_point: boolean + is_dropoff_point: boolean + created_at?: string +} + +export interface BusSchedule { + id: string + route_id: string + departure_time: string + frequency_minutes?: number + schedule_type: ScheduleType + is_active: boolean + is_published: boolean + notes?: string + created_at?: string +} + +export interface Coupon { + id: string + business_id?: string | null + title: string + description?: string | null + business_name?: string | null + business_address?: string | null + business_phone?: string | null + image_url?: string | null + social_media?: string | null + terms?: string | null + discount_percentage?: number | null + discount_amount?: number | null + category?: string | null + valid_from?: string | null + valid_until?: string | null + is_active: boolean + business?: Business | null + created_at?: string + updated_at?: string +} + +export interface Business { + id: string + name: string + address?: string | null + phone?: string | null + image_url?: string | null + social_media?: string | null + category?: string | null + latitude?: number | null + longitude?: number | null + area?: string | null + updated_at?: string +} + +export type UserCouponStatus = 'claimed' | 'redeemed' | 'expired' + +export interface UserCoupon { + id: string + user_id: string + coupon_id: string + status: UserCouponStatus + redemption_code: string + claimed_at: string + redeemed_at?: string | null + coupon?: Coupon +} + +export interface Taxi { + id: string + owner_name: string + phone_number: string + license_plate: string + cooperative?: string + corregimiento: string + shift: string + rating?: number + english_speaking?: boolean + image_url?: string + is_active: boolean + created_at?: string + updated_at?: string +} + +export interface Favorite { + id: string + user_id: string + item_type: 'route' | 'stop' | 'taxi' + item_id: string + created_at?: string +} + +export type BusStopCreate = Omit +export type BusStopUpdate = Partial + +export type RouteCreate = Omit +export type RouteUpdate = Partial + +export interface RouteStopCreate { + stop_id: string + stop_order?: number + travel_time_minutes?: number + stop_delay_minutes?: number + is_pickup_point?: boolean + is_dropoff_point?: boolean +} + +export interface RouteStopUpdate { + stop_order?: number + travel_time_minutes?: number + stop_delay_minutes?: number + is_pickup_point?: boolean + is_dropoff_point?: boolean +} + +export interface Shuttle { + id: string + route_name: string + description?: string + origin: string + destination: string + vehicle_type: string + company_name?: string + trip_type: 'one_way' | 'round_trip' | 'both' + price_per_person?: number + price_private_trip?: number + estimated_duration: string + departure_times?: string + contact_whatsapp: string + phone_number?: string + english_speaking?: boolean + image_url?: string + is_active: boolean + created_at?: string + updated_at?: string +} diff --git a/frontend/src/utils/timeFormatter.ts b/frontend/src/utils/timeFormatter.ts new file mode 100644 index 0000000..029b169 --- /dev/null +++ b/frontend/src/utils/timeFormatter.ts @@ -0,0 +1,21 @@ +/** + * Formats a time string (e.g., "14:30:00" or "14:30") to 12-hour format (e.g., "02:30 PM"). + * Omits seconds. + */ +export function formatTo12Hour(timeStr: string | undefined | null): string { + if (!timeStr) return ''; + + const [h, m] = timeStr.split(':'); + if (h === undefined || m === undefined) return timeStr; + + let hours = parseInt(h, 10); + const minutes = m; + + const ampm = hours >= 12 ? 'PM' : 'AM'; + + hours = hours % 12; + hours = hours ? hours : 12; // the hour '0' should be '12' + + // Ensure 2-digit minutes and return + return `${hours.toString().padStart(2, '0')}:${minutes.substring(0, 2)} ${ampm}`; +} diff --git a/frontend/src/views/AdminBusStops.vue b/frontend/src/views/AdminBusStops.vue new file mode 100644 index 0000000..09fe99b --- /dev/null +++ b/frontend/src/views/AdminBusStops.vue @@ -0,0 +1,253 @@ + + + + + diff --git a/frontend/src/views/AdminDashboard.vue b/frontend/src/views/AdminDashboard.vue new file mode 100644 index 0000000..88c0a27 --- /dev/null +++ b/frontend/src/views/AdminDashboard.vue @@ -0,0 +1,521 @@ + + + + + diff --git a/frontend/src/views/AdminDrivers.vue b/frontend/src/views/AdminDrivers.vue new file mode 100644 index 0000000..62baa03 --- /dev/null +++ b/frontend/src/views/AdminDrivers.vue @@ -0,0 +1,1163 @@ + + + + + diff --git a/frontend/src/views/AdminPanel.vue b/frontend/src/views/AdminPanel.vue new file mode 100644 index 0000000..b0ca703 --- /dev/null +++ b/frontend/src/views/AdminPanel.vue @@ -0,0 +1,280 @@ + + + + + diff --git a/frontend/src/views/AdminReports.vue b/frontend/src/views/AdminReports.vue new file mode 100644 index 0000000..9a4089b --- /dev/null +++ b/frontend/src/views/AdminReports.vue @@ -0,0 +1,318 @@ + + + + + diff --git a/frontend/src/views/AdminRoutes.vue b/frontend/src/views/AdminRoutes.vue new file mode 100644 index 0000000..c5e5e3d --- /dev/null +++ b/frontend/src/views/AdminRoutes.vue @@ -0,0 +1,592 @@ + + + + + diff --git a/frontend/src/views/AdminSchedules.vue b/frontend/src/views/AdminSchedules.vue new file mode 100644 index 0000000..8181c13 --- /dev/null +++ b/frontend/src/views/AdminSchedules.vue @@ -0,0 +1,613 @@ + + + + + diff --git a/frontend/src/views/AdminShuttles.vue b/frontend/src/views/AdminShuttles.vue new file mode 100644 index 0000000..2e3d830 --- /dev/null +++ b/frontend/src/views/AdminShuttles.vue @@ -0,0 +1,701 @@ + + + + + diff --git a/frontend/src/views/AdminTaxis.vue b/frontend/src/views/AdminTaxis.vue new file mode 100644 index 0000000..40f351b --- /dev/null +++ b/frontend/src/views/AdminTaxis.vue @@ -0,0 +1,716 @@ + + + + + diff --git a/frontend/src/views/AuthView.vue b/frontend/src/views/AuthView.vue new file mode 100644 index 0000000..96a275d --- /dev/null +++ b/frontend/src/views/AuthView.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/frontend/src/views/BusStopDetailsView.vue b/frontend/src/views/BusStopDetailsView.vue new file mode 100644 index 0000000..a3779d0 --- /dev/null +++ b/frontend/src/views/BusStopDetailsView.vue @@ -0,0 +1,93 @@ + + + + + + diff --git a/frontend/src/views/BusinessDetailsView.vue b/frontend/src/views/BusinessDetailsView.vue new file mode 100644 index 0000000..243a958 --- /dev/null +++ b/frontend/src/views/BusinessDetailsView.vue @@ -0,0 +1,485 @@ + + + + + diff --git a/frontend/src/views/CouponsView.vue b/frontend/src/views/CouponsView.vue new file mode 100644 index 0000000..504390c --- /dev/null +++ b/frontend/src/views/CouponsView.vue @@ -0,0 +1,646 @@ + + + + + + diff --git a/frontend/src/views/DiscoverView.vue b/frontend/src/views/DiscoverView.vue new file mode 100644 index 0000000..c542ed0 --- /dev/null +++ b/frontend/src/views/DiscoverView.vue @@ -0,0 +1,588 @@ + + + + + diff --git a/frontend/src/views/DriverDashboard.vue b/frontend/src/views/DriverDashboard.vue new file mode 100644 index 0000000..4771da7 --- /dev/null +++ b/frontend/src/views/DriverDashboard.vue @@ -0,0 +1,403 @@ + + + + + diff --git a/frontend/src/views/FavoritesView.vue b/frontend/src/views/FavoritesView.vue new file mode 100644 index 0000000..659a13c --- /dev/null +++ b/frontend/src/views/FavoritesView.vue @@ -0,0 +1,596 @@ + + + + + diff --git a/frontend/src/views/MapView.vue b/frontend/src/views/MapView.vue new file mode 100644 index 0000000..3184fb6 --- /dev/null +++ b/frontend/src/views/MapView.vue @@ -0,0 +1,2131 @@ + + + + + diff --git a/frontend/src/views/ProfileView.vue b/frontend/src/views/ProfileView.vue new file mode 100644 index 0000000..95a19a7 --- /dev/null +++ b/frontend/src/views/ProfileView.vue @@ -0,0 +1,720 @@ + + + + + diff --git a/frontend/src/views/PromoterDashboard.vue b/frontend/src/views/PromoterDashboard.vue new file mode 100644 index 0000000..adebbdd --- /dev/null +++ b/frontend/src/views/PromoterDashboard.vue @@ -0,0 +1,1188 @@ + + + + + diff --git a/frontend/src/views/RoutesView.vue b/frontend/src/views/RoutesView.vue new file mode 100644 index 0000000..7635d76 --- /dev/null +++ b/frontend/src/views/RoutesView.vue @@ -0,0 +1,288 @@ + + + + + diff --git a/frontend/src/views/SchedulesView.vue b/frontend/src/views/SchedulesView.vue new file mode 100644 index 0000000..c569e97 --- /dev/null +++ b/frontend/src/views/SchedulesView.vue @@ -0,0 +1,472 @@ + + + + + + diff --git a/frontend/src/views/SplashScreen.vue b/frontend/src/views/SplashScreen.vue new file mode 100644 index 0000000..5a0e491 --- /dev/null +++ b/frontend/src/views/SplashScreen.vue @@ -0,0 +1,241 @@ + + + + + + diff --git a/frontend/src/views/StrategicAnalytics.vue b/frontend/src/views/StrategicAnalytics.vue new file mode 100644 index 0000000..4e3635d --- /dev/null +++ b/frontend/src/views/StrategicAnalytics.vue @@ -0,0 +1,482 @@ + + + + + diff --git a/frontend/src/views/TaxiView.vue b/frontend/src/views/TaxiView.vue new file mode 100644 index 0000000..c7746f2 --- /dev/null +++ b/frontend/src/views/TaxiView.vue @@ -0,0 +1,959 @@ + + + + + diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..1bbebb0 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,20 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": ["vite/client", "google.maps"], + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/frontend/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/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..09fd44f --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,87 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import VueDevTools from 'vite-plugin-vue-devtools' +import { VitePWA } from 'vite-plugin-pwa' +import path from 'path' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + VueDevTools(), + VitePWA({ + registerType: 'autoUpdate', + includeAssets: ['icon-192.png', 'icon-512.png', 'icon-1024.png', 'favicon.ico'], + manifest: { + name: 'SIBU - Sistema de Transporte', + short_name: 'SIBU', + description: 'Sistema de Transporte Público', + theme_color: '#fee715', + background_color: '#ffffff', + display: 'standalone', + orientation: 'portrait', + scope: '/', + start_url: '/', + icons: [ + { + src: 'icon-192.png', + sizes: '192x192', + type: 'image/png', + }, + { + src: 'icon-512.png', + sizes: '512x512', + type: 'image/png', + }, + { + src: 'icon-1024.png', + sizes: '1024x1024', + type: 'image/png', + }, + ], + }, + workbox: { + globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'], + runtimeCaching: [ + { + urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'google-fonts-cache', + expiration: { + maxEntries: 10, + maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year + }, + cacheableResponse: { + statuses: [0, 200], + }, + }, + }, + { + urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'gstatic-fonts-cache', + expiration: { + maxEntries: 10, + maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year + }, + cacheableResponse: { + statuses: [0, 200], + }, + }, + }, + ], + }, + devOptions: { + enabled: false, + type: 'module', + }, + }), + ], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +}) diff --git a/old/.gitignore b/old/.gitignore new file mode 100644 index 0000000..185a2e6 --- /dev/null +++ b/old/.gitignore @@ -0,0 +1,47 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ +pubspec.lock + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/web/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/old/.metadata b/old/.metadata new file mode 100644 index 0000000..e679346 --- /dev/null +++ b/old/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "19074d12f7eaf6a8180cd4036a430c1d76de904e" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + - platform: web + create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/old/QUICK-START-POSTGRESQL.md b/old/QUICK-START-POSTGRESQL.md new file mode 100644 index 0000000..4e9f2c6 --- /dev/null +++ b/old/QUICK-START-POSTGRESQL.md @@ -0,0 +1,116 @@ +# Quick Start: PostgreSQL Setup + +## ✅ What's Been Configured + +1. **Backend Database Connection**: Configured to use `postgresql+asyncpg://sibu:sibu@localhost:5432/sibu` +2. **Environment File**: Created `backend/.env.development` with database settings +3. **API Client**: Created Flutter `ApiClient` service for backend communication +4. **Run Scripts**: Created helper scripts for easy execution + +## 🚀 Quick Start + +### 1. Ensure PostgreSQL is Running + +```bash +# Check if PostgreSQL is running +psql -h localhost -p 5432 -U sibu -d sibu -c "SELECT version();" +``` + +If it fails, start PostgreSQL: +```bash +# macOS with Homebrew +brew services start postgresql@14 # or your version +``` + +### 2. Create Database (if needed) + +```bash +psql -h localhost -p 5432 -U postgres -c "CREATE DATABASE sibu;" +psql -h localhost -p 5432 -U postgres -c "CREATE USER sibu WITH PASSWORD 'sibu';" +psql -h localhost -p 5432 -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE sibu TO sibu;" +``` + +### 3. Apply Database Schema + +You have migrations in `supabase/migrations/`. Apply them: + +```bash +cd old +# Apply all migrations +for migration in supabase/migrations/*.sql; do + psql -h localhost -p 5432 -U sibu -d sibu -f "$migration" +done +``` + +Or use Alembic (if backend models match): +```bash +cd backend +uv run alembic upgrade head +``` + +### 4. Start Backend API + +```bash +cd backend +uv run fastapi dev app/main.py +``` + +Backend will be available at: `http://localhost:8000` + +### 5. Run Flutter App + +In a new terminal: + +```bash +cd old +./scripts/run-flutter-backend.sh +``` + +Or manually: +```bash +flutter run -d chrome --dart-define=API_BASE_URL=http://localhost:8000 +``` + +## 📋 Configuration Summary + +| Component | Configuration | +|-----------|--------------| +| **Database** | `sibu:sibu@localhost:5432/sibu` | +| **Backend API** | `http://localhost:8000` | +| **Flutter API Client** | Configured via `--dart-define=API_BASE_URL` | + +## 🔍 Verify Setup + +### Test Database Connection +```bash +psql -h localhost -p 5432 -U sibu -d sibu -c "\dt" # List tables +``` + +### Test Backend API +```bash +curl http://localhost:8000/health +curl http://localhost:8000/api/routes +``` + +### Test Flutter Connection +The app will automatically try to connect to the backend API on startup. + +## 📚 Next Steps + +- See `README-POSTGRESQL-SETUP.md` for detailed documentation +- Update Flutter services to use `ApiClient` instead of `SupabaseService` (currently still using Supabase) +- Add authentication if needed +- Configure CORS properly for production + +## 🐛 Troubleshooting + +**Backend can't connect:** +- Check PostgreSQL is running: `lsof -i :5432` +- Verify credentials: `psql -h localhost -p 5432 -U sibu -d sibu` +- Check `.env.development` file exists + +**Flutter can't connect:** +- Verify backend is running: `curl http://localhost:8000/health` +- Check API_BASE_URL is set correctly +- Check browser console for CORS errors + diff --git a/old/README-POSTGRESQL-SETUP.md b/old/README-POSTGRESQL-SETUP.md new file mode 100644 index 0000000..969a827 --- /dev/null +++ b/old/README-POSTGRESQL-SETUP.md @@ -0,0 +1,210 @@ +# PostgreSQL Direct Connection Setup + +This guide explains how to use the PostgreSQL database directly instead of Supabase. + +## Architecture + +Since Flutter web cannot connect directly to PostgreSQL from the browser (security restrictions), we use a two-tier architecture: + +``` +Flutter App (Web) → FastAPI Backend → PostgreSQL Database +``` + +The backend acts as an API layer that connects to PostgreSQL and exposes REST endpoints. + +## Database Configuration + +The PostgreSQL database is configured as: +- **Host**: localhost +- **Port**: 5432 +- **Database**: sibu +- **Username**: sibu +- **Password**: sibu + +Connection string: `postgresql+asyncpg://sibu:sibu@localhost:5432/sibu` + +## Setup Steps + +### 1. Ensure PostgreSQL is Running + +Make sure your PostgreSQL database is running and accessible: + +```bash +# Test connection (if you have psql installed) +psql -h localhost -p 5432 -U sibu -d sibu +``` + +### 2. Apply Database Migrations + +The database schema needs to be created. You have two options: + +#### Option A: Use Supabase Migrations + +If you have the Supabase migrations in `supabase/migrations/`, you can apply them directly: + +```bash +# Connect to PostgreSQL and run migrations +psql -h localhost -p 5432 -U sibu -d sibu -f supabase/migrations/20241019215951_sibu_transportation_system.sql +# ... apply other migrations +``` + +#### Option B: Use Alembic (Backend Migrations) + +The backend uses Alembic for migrations: + +```bash +cd backend +uv run alembic upgrade head +``` + +### 3. Start the Backend API + +```bash +cd backend +uv run fastapi dev app/main.py +``` + +The API will be available at `http://localhost:8000` + +### 4. Run Flutter App + +```bash +cd old +./scripts/run-flutter-backend.sh +``` + +Or manually: + +```bash +flutter run -d chrome --dart-define=API_BASE_URL=http://localhost:8000 +``` + +## Backend API Endpoints + +The backend provides the following endpoints: + +- `GET /api/routes` - Get all routes +- `GET /api/routes/{route_id}` - Get specific route +- `GET /api/bus-stops` - Get all bus stops +- `GET /api/bus-stops/{stop_id}` - Get specific bus stop +- `GET /api/schedules` - Get schedules (with optional route_id, stop_id filters) +- `GET /api/coupons` - Get coupons (with optional category, is_active filters) +- `GET /api/taxis` - Get taxis (with optional filters) +- `GET /health` - Health check + +## Configuration Files + +### Backend Configuration + +**File**: `backend/.env.development` +```env +DATABASE_URL=postgresql+asyncpg://sibu:sibu@localhost:5432/sibu +ENVIRONMENT=development +DEBUG=true +``` + +**File**: `backend/app/core/config.py` +- Default database URL is set to your PostgreSQL connection + +### Flutter Configuration + +The Flutter app uses `ApiClient` service which: +- Defaults to `http://localhost:8000` +- Can be configured via `--dart-define=API_BASE_URL=` + +## Switching Between Supabase and PostgreSQL + +### Use PostgreSQL (via Backend API) +```bash +cd old +./scripts/run-flutter-backend.sh +``` + +### Use Supabase +```bash +cd old +./scripts/run-flutter-local.sh # For local Supabase +# OR +flutter run -d chrome \ + --dart-define=SUPABASE_URL= \ + --dart-define=SUPABASE_ANON_KEY= +``` + +## Troubleshooting + +### Backend Can't Connect to PostgreSQL + +1. **Check PostgreSQL is running:** + ```bash + # macOS + brew services list | grep postgresql + + # Or check if port is listening + lsof -i :5432 + ``` + +2. **Verify credentials:** + ```bash + psql -h localhost -p 5432 -U sibu -d sibu + ``` + +3. **Check database exists:** + ```sql + \l -- List databases + ``` + +### Flutter Can't Connect to Backend + +1. **Check backend is running:** + ```bash + curl http://localhost:8000/health + ``` + +2. **Check CORS settings** in `backend/app/main.py` + +3. **Verify API URL** in Flutter: + ```dart + // Check ApiClient base URL + print(ApiClient.instance._baseUrl); + ``` + +### Database Schema Issues + +If tables don't exist: + +1. **Apply migrations:** + ```bash + cd backend + uv run alembic upgrade head + ``` + +2. **Or manually create from Supabase migrations:** + ```bash + psql -h localhost -p 5432 -U sibu -d sibu < supabase/migrations/20241019215951_sibu_transportation_system.sql + ``` + +## Development Workflow + +1. **Start PostgreSQL** (if not running as a service) +2. **Start Backend API:** + ```bash + cd backend + uv run fastapi dev app/main.py + ``` +3. **Run Flutter App:** + ```bash + cd old + ./scripts/run-flutter-backend.sh + ``` + +## Next Steps + +The Flutter app services need to be updated to use `ApiClient` instead of `SupabaseService`. Currently, the app still uses Supabase. To fully migrate: + +1. Update `TransportationService` to use `ApiClient` +2. Update `CouponService` to use `ApiClient` +3. Update `TaxiService` to use `ApiClient` +4. Remove or make optional `SupabaseService` + +This migration can be done incrementally. + diff --git a/old/README-SUPABASE-LOCAL.md b/old/README-SUPABASE-LOCAL.md new file mode 100644 index 0000000..6979539 --- /dev/null +++ b/old/README-SUPABASE-LOCAL.md @@ -0,0 +1,196 @@ +# Local Supabase Setup Guide + +This guide will help you set up and run Supabase locally for development. + +## Prerequisites + +1. **Docker Desktop** - Supabase runs on Docker + - Download: https://docs.docker.com/desktop + - Make sure Docker Desktop is running before proceeding + +2. **Supabase CLI** - Already installed via Homebrew + +## Quick Start + +### 1. Start Local Supabase + +```bash +cd old +./scripts/setup-local-supabase.sh +``` + +This script will: +- Check if Docker is running +- Initialize Supabase (if not already done) +- Start all Supabase services locally +- Apply all database migrations +- Display your local credentials + +### 2. Run Flutter App with Local Supabase + +```bash +./scripts/run-flutter-local.sh +``` + +Or specify a device: +```bash +./scripts/run-flutter-local.sh chrome +./scripts/run-flutter-local.sh web-server +``` + +### 3. Get Credentials Manually + +If you need to see the credentials again: + +```bash +./scripts/get-local-credentials.sh +``` + +Or use the Supabase CLI directly: +```bash +supabase status +``` + +## Manual Setup + +If you prefer to set up manually: + +### Step 1: Start Supabase + +```bash +cd old +supabase start +``` + +### Step 2: Get Credentials + +After starting, `supabase status` will show: +- **API URL**: Your local Supabase URL (usually `http://127.0.0.1:54321`) +- **anon key**: Your local anon key + +### Step 3: Run Flutter with Credentials + +```bash +flutter run -d chrome \ + --dart-define=SUPABASE_URL="http://127.0.0.1:54321" \ + --dart-define=SUPABASE_ANON_KEY="" +``` + +## Useful Commands + +### Supabase Management + +```bash +# Start Supabase +supabase start + +# Stop Supabase +supabase stop + +# View Supabase status and credentials +supabase status + +# View logs +supabase logs + +# Reset database (applies all migrations fresh) +supabase db reset + +# Apply new migrations +supabase db reset +``` + +### Database Migrations + +Migrations are automatically applied when you run `supabase start` or `supabase db reset`. + +To create a new migration: +```bash +supabase migration new +``` + +## Local Supabase Services + +When running locally, Supabase provides: + +- **API**: `http://127.0.0.1:54321` +- **Studio (Admin UI)**: `http://127.0.0.1:54323` +- **Database**: `postgresql://postgres:postgres@127.0.0.1:54322/postgres` +- **Auth**: Handled by the API +- **Storage**: Handled by the API + +## Accessing Supabase Studio + +Supabase Studio is a web-based admin interface for managing your local database: + +1. Start Supabase: `supabase start` +2. Open Studio URL from `supabase status` output (usually `http://127.0.0.1:54323`) +3. Use it to: + - View and edit tables + - Run SQL queries + - Manage authentication + - View API documentation + +## Troubleshooting + +### Docker Not Running + +``` +Error: Cannot connect to the Docker daemon +``` + +**Solution**: Start Docker Desktop and wait for it to fully start, then try again. + +### Port Already in Use + +If ports 54321, 54322, or 54323 are already in use: + +1. Stop the conflicting service +2. Or modify `supabase/config.toml` to use different ports + +### Database Reset Issues + +If migrations fail: + +```bash +# Stop Supabase +supabase stop + +# Reset everything +supabase db reset + +# Start again +supabase start +``` + +### Flutter Can't Connect + +Make sure: +1. Supabase is running (`supabase status`) +2. You're using the correct URL and anon key +3. The `--dart-define` flags are correctly formatted + +## Switching Between Local and Production + +### Use Local Supabase +```bash +./scripts/run-flutter-local.sh +``` + +### Use Production Supabase +Update `env.json` with production credentials and run: +```bash +flutter run -d chrome \ + --dart-define-from-file=env.json +``` + +Note: The current app uses `String.fromEnvironment`, so you need `--dart-define` flags. + +## Next Steps + +1. ✅ Start Docker Desktop +2. ✅ Run `./scripts/setup-local-supabase.sh` +3. ✅ Run `./scripts/run-flutter-local.sh` +4. ✅ Open Supabase Studio to view your database +5. ✅ Start developing! + diff --git a/old/README-SUPABASE-PRODUCTION.md b/old/README-SUPABASE-PRODUCTION.md new file mode 100644 index 0000000..7ca36c2 --- /dev/null +++ b/old/README-SUPABASE-PRODUCTION.md @@ -0,0 +1,103 @@ +# Supabase Production Setup (No Docker) + +This guide shows how to use Supabase production instance (no local Docker required). + +## Quick Start + +### Run Flutter with Supabase + +```bash +cd old +./scripts/run-flutter-supabase.sh +``` + +This script: +1. Reads `env.json` for Supabase credentials +2. Extracts `SUPABASE_URL` and `SUPABASE_ANON_KEY` +3. Runs Flutter with `--dart-define` flags + +### Manual Run + +If you prefer to run manually: + +```bash +flutter run -d chrome \ + --dart-define=SUPABASE_URL="https://yeqdxdocspsuexamljen.supabase.co" \ + --dart-define=SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +## Configuration + +Your Supabase credentials are stored in `env.json`: + +```json +{ + "SUPABASE_URL": "https://yeqdxdocspsuexamljen.supabase.co", + "SUPABASE_ANON_KEY": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} +``` + +## How It Works + +1. **Script reads env.json** → Extracts Supabase credentials +2. **Flutter receives credentials** → Via `--dart-define` flags +3. **App initializes Supabase** → Uses `String.fromEnvironment()` to read credentials +4. **Services use Supabase** → All data operations go through Supabase client + +## Troubleshooting + +### "Missing SUPABASE_URL or SUPABASE_ANON_KEY" + +**Solution**: Make sure you're running with the script: +```bash +./scripts/run-flutter-supabase.sh +``` + +Or manually pass the flags: +```bash +flutter run -d chrome \ + --dart-define=SUPABASE_URL="" \ + --dart-define=SUPABASE_ANON_KEY="" +``` + +### Script Can't Read env.json + +Make sure: +1. `env.json` exists in the `old/` directory +2. File contains valid JSON +3. Has `SUPABASE_URL` and `SUPABASE_ANON_KEY` keys + +### Supabase Connection Errors + +1. **Check your Supabase project is active:** + - Go to https://supabase.com/dashboard + - Verify your project is running + +2. **Verify credentials:** + - Check `env.json` has correct values + - Make sure anon key hasn't been rotated + +3. **Check network:** + - Ensure you can access `https://yeqdxdocspsuexamljen.supabase.co` + +## Database Access + +Your Supabase project has a PostgreSQL database. You can: + +1. **Access via Supabase Dashboard:** + - Go to https://supabase.com/dashboard + - Select your project + - Use SQL Editor or Table Editor + +2. **Direct PostgreSQL connection:** + - Connection string available in Supabase Dashboard → Settings → Database + - Use with psql or any PostgreSQL client + +## Next Steps + +- ✅ Supabase is configured and ready +- ✅ Run `./scripts/run-flutter-supabase.sh` to start the app +- ✅ App will connect to your production Supabase instance + +No Docker needed! 🎉 + diff --git a/old/README.md b/old/README.md new file mode 100644 index 0000000..2e12b6c --- /dev/null +++ b/old/README.md @@ -0,0 +1,142 @@ +# Flutter + +A modern Flutter-based mobile application utilizing the latest mobile development technologies and tools for building responsive cross-platform applications. + +## 📋 Prerequisites + +- Flutter SDK (^3.29.2) +- Dart SDK +- Android Studio / VS Code with Flutter extensions +- Android SDK / Xcode (for iOS development) + +## 🛠️ Installation + +1. Install dependencies: +```bash +flutter pub get +``` + +2. Run the application: + +To run the app with environment variables defined in an env.json file, follow the steps mentioned below: +1. Through CLI + ```bash + flutter run --dart-define-from-file=env.json + ``` +2. For VSCode + - Open .vscode/launch.json (create it if it doesn't exist). + - Add or modify your launch configuration to include --dart-define-from-file: + ```json + { + "version": "0.2.0", + "configurations": [ + { + "name": "Launch", + "request": "launch", + "type": "dart", + "program": "lib/main.dart", + "args": [ + "--dart-define-from-file", + "env.json" + ] + } + ] + } + ``` +3. For IntelliJ / Android Studio + - Go to Run > Edit Configurations. + - Select your Flutter configuration or create a new one. + - Add the following to the "Additional arguments" field: + ```bash + --dart-define-from-file=env.json + ``` + +## 📁 Project Structure + +``` +flutter_app/ +├── android/ # Android-specific configuration +├── ios/ # iOS-specific configuration +├── lib/ +│ ├── core/ # Core utilities and services +│ │ └── utils/ # Utility classes +│ ├── presentation/ # UI screens and widgets +│ │ └── splash_screen/ # Splash screen implementation +│ ├── routes/ # Application routing +│ ├── theme/ # Theme configuration +│ ├── widgets/ # Reusable UI components +│ └── main.dart # Application entry point +├── assets/ # Static assets (images, fonts, etc.) +├── pubspec.yaml # Project dependencies and configuration +└── README.md # Project documentation +``` + +## 🧩 Adding Routes + +To add new routes to the application, update the `lib/routes/app_routes.dart` file: + +```dart +import 'package:flutter/material.dart'; +import 'package:package_name/presentation/home_screen/home_screen.dart'; + +class AppRoutes { + static const String initial = '/'; + static const String home = '/home'; + + static Map routes = { + initial: (context) => const SplashScreen(), + home: (context) => const HomeScreen(), + // Add more routes as needed + } +} +``` + +## 🎨 Theming + +This project includes a comprehensive theming system with both light and dark themes: + +```dart +// Access the current theme +ThemeData theme = Theme.of(context); + +// Use theme colors +Color primaryColor = theme.colorScheme.primary; +``` + +The theme configuration includes: +- Color schemes for light and dark modes +- Typography styles +- Button themes +- Input decoration themes +- Card and dialog themes + +## 📱 Responsive Design + +The app is built with responsive design using the Sizer package: + +```dart +// Example of responsive sizing +Container( + width: 50.w, // 50% of screen width + height: 20.h, // 20% of screen height + child: Text('Responsive Container'), +) +``` +## 📦 Deployment + +Build the application for production: + +```bash +# For Android +flutter build apk --release + +# For iOS +flutter build ios --release +``` + +## 🙏 Acknowledgments +- Built with [Rocket.new](https://rocket.new) +- Powered by [Flutter](https://flutter.dev) & [Dart](https://dart.dev) +- Styled with Material Design + +Built with ❤️ on Rocket.new diff --git a/old/analysis_options.yaml b/old/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/old/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/old/android/.gitignore b/old/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/old/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/old/android/app/build.gradle b/old/android/app/build.gradle new file mode 100644 index 0000000..4583729 --- /dev/null +++ b/old/android/app/build.gradle @@ -0,0 +1,56 @@ +plugins { + id "com.android.application" + id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id "dev.flutter.flutter-gradle-plugin" +} + +android { + namespace = "com.sibu.app" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + coreLibraryDesugaringEnabled true + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.sibu.app" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + multiDexEnabled true + minSdkVersion 23 + } + + buildTypes { + release { + minifyEnabled false + shrinkResources false + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4' + implementation "androidx.multidex:multidex:2.0.1" + implementation 'com.google.android.material:material:1.9.0' + implementation 'androidx.concurrent:concurrent-futures:1.1.0' +} diff --git a/old/android/app/proguard-rules.pro b/old/android/app/proguard-rules.pro new file mode 100644 index 0000000..e53c7a4 --- /dev/null +++ b/old/android/app/proguard-rules.pro @@ -0,0 +1,7 @@ +-dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivity$g +-dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivityStarter$Args +-dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivityStarter$Error +-dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivityStarter +-dontwarn com.stripe.android.pushProvisioning.PushProvisioningEphemeralKeyProvider +# Keep Stripe classes +-keep class com.stripe.** { *; } \ No newline at end of file diff --git a/old/android/app/src/debug/AndroidManifest.xml b/old/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..f880684 --- /dev/null +++ b/old/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + diff --git a/old/android/app/src/main/AndroidManifest.xml b/old/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..880b2aa --- /dev/null +++ b/old/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/old/android/app/src/main/kotlin/com/sibu/app/MainActivity.kt b/old/android/app/src/main/kotlin/com/sibu/app/MainActivity.kt new file mode 100644 index 0000000..6924971 --- /dev/null +++ b/old/android/app/src/main/kotlin/com/sibu/app/MainActivity.kt @@ -0,0 +1,7 @@ +package com.sibu.app + +import io.flutter.embedding.android.FlutterFragmentActivity +import io.flutter.plugins.GeneratedPluginRegistrant + +class MainActivity: FlutterFragmentActivity() { +} \ No newline at end of file diff --git a/old/android/app/src/main/res/drawable-v21/launch_background.xml b/old/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/old/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/old/android/app/src/main/res/drawable/launch_background.xml b/old/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/old/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/old/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/old/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/old/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/old/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/old/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/old/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/old/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/old/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/old/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/old/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/old/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/old/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/old/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/old/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/old/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/old/android/app/src/main/res/values-night/styles.xml b/old/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..406b93a --- /dev/null +++ b/old/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/old/android/app/src/main/res/values/strings.xml b/old/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..72a43e4 --- /dev/null +++ b/old/android/app/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + sibu + + \ No newline at end of file diff --git a/old/android/app/src/main/res/values/styles.xml b/old/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..0ee215c --- /dev/null +++ b/old/android/app/src/main/res/values/styles.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/old/android/app/src/profile/AndroidManifest.xml b/old/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..f880684 --- /dev/null +++ b/old/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + diff --git a/old/android/build.gradle b/old/android/build.gradle new file mode 100644 index 0000000..8f31e8c --- /dev/null +++ b/old/android/build.gradle @@ -0,0 +1,18 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/old/android/gradle.properties b/old/android/gradle.properties new file mode 100644 index 0000000..84044a9 --- /dev/null +++ b/old/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=false diff --git a/old/android/gradle/wrapper/gradle-wrapper.properties b/old/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..514d9bb --- /dev/null +++ b/old/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-all.zip diff --git a/old/android/settings.gradle b/old/android/settings.gradle new file mode 100644 index 0000000..f4e0879 --- /dev/null +++ b/old/android/settings.gradle @@ -0,0 +1,25 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.6.0" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false +} + +include ":app" \ No newline at end of file diff --git a/old/assets/images/img_app_logo.svg b/old/assets/images/img_app_logo.svg new file mode 100644 index 0000000..db16989 --- /dev/null +++ b/old/assets/images/img_app_logo.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/old/assets/images/no-image.jpg b/old/assets/images/no-image.jpg new file mode 100644 index 0000000..b42dd2f Binary files /dev/null and b/old/assets/images/no-image.jpg differ diff --git a/old/assets/images/sad_face.svg b/old/assets/images/sad_face.svg new file mode 100644 index 0000000..bebf0cf --- /dev/null +++ b/old/assets/images/sad_face.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/old/env.json b/old/env.json new file mode 100644 index 0000000..fec7ef8 --- /dev/null +++ b/old/env.json @@ -0,0 +1,10 @@ +{ + "SUPABASE_URL": "https://yeqdxdocspsuexamljen.supabase.co", + "SUPABASE_ANON_KEY": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InllcWR4ZG9jc3BzdWV4YW1samVuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjA4Njk4MzksImV4cCI6MjA3NjQ0NTgzOX0.CoHnreH3DPB4Ut2dCuIjGylQJzsOx_4qF1mAgGuf_Yk", + "OPENAI_API_KEY": "your-openai-api-key-here", + "GEMINI_API_KEY": "your-gemini-api-key-here", + "ANTHROPIC_API_KEY": "your-anthropic-api-key-here", + "PERPLEXITY_API_KEY": "your-perplexity-api-key-here", + "GOOGLE_WEB_CLIENT_ID": "your_google_web_client_id", + "GOOGLE_MAPS_API_KEY": "your-google-maps-api-key" +} \ No newline at end of file diff --git a/old/ios/.gitignore b/old/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/old/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/old/ios/Flutter/AppFrameworkInfo.plist b/old/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9625e10 --- /dev/null +++ b/old/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/old/ios/Flutter/Debug.xcconfig b/old/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/old/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/old/ios/Flutter/Release.xcconfig b/old/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/old/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/old/ios/Podfile b/old/ios/Podfile new file mode 100644 index 0000000..e549ee2 --- /dev/null +++ b/old/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/old/ios/Runner.xcodeproj/project.pbxproj b/old/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..f8c8a8b --- /dev/null +++ b/old/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,617 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807E294A63A400263BE5 /* Frameworks */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 8GZ776NSU2; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.sibu.app.testProject; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AE0B7B92F70575B8D7E0D07E /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.sibu.app.testProject.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 89B67EB44CE7B6631473024E /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.sibu.app.testProject.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 640959BDD8F10B91D80A66BE /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.sibu.app.testProject.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 8GZ776NSU2; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.sibu.app.testProject; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 8GZ776NSU2; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.sibu.app.testProject; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/old/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/old/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/old/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/old/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/old/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/old/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/old/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/old/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/old/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/old/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/old/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..87131a0 --- /dev/null +++ b/old/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/old/ios/Runner.xcworkspace/contents.xcworkspacedata b/old/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/old/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/old/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/old/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/old/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/old/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/old/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/old/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/old/ios/Runner/AppDelegate.swift b/old/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..dfc017c --- /dev/null +++ b/old/ios/Runner/AppDelegate.swift @@ -0,0 +1,17 @@ +import UIKit +import Flutter +import GoogleMaps + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + // Initialize Google Maps with API key + GMSServices.provideAPIKey(AIzaSyAwcCgnmNvsTDyELAlBPWjdzXb1D2rosq8) + + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} \ No newline at end of file diff --git a/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/old/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/old/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/old/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/old/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/old/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/old/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/old/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/old/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/old/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/old/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/old/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/old/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/old/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/old/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/old/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/old/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/old/ios/Runner/Base.lproj/LaunchScreen.storyboard b/old/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/old/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/old/ios/Runner/Base.lproj/Main.storyboard b/old/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/old/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/old/ios/Runner/Info.plist b/old/ios/Runner/Info.plist new file mode 100644 index 0000000..424524a --- /dev/null +++ b/old/ios/Runner/Info.plist @@ -0,0 +1,55 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + SIBU + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + SIBU + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + + NSLocationWhenInUseUsageDescription + Necesitamos tu ubicación para mostrarte las paradas de bus cercanas + NSLocationAlwaysAndWhenInUseUsageDescription + Necesitamos tu ubicación para mostrarte las paradas de bus cercanas + + \ No newline at end of file diff --git a/old/ios/Runner/Runner-Bridging-Header.h b/old/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/old/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/old/ios/RunnerTests/RunnerTests.swift b/old/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/old/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/old/lib/core/app_export.dart b/old/lib/core/app_export.dart new file mode 100644 index 0000000..7ea9914 --- /dev/null +++ b/old/lib/core/app_export.dart @@ -0,0 +1,6 @@ +export 'package:connectivity_plus/connectivity_plus.dart'; +export 'package:google_fonts/google_fonts.dart'; +export '../routes/app_routes.dart'; +export '../widgets/custom_icon_widget.dart'; +export '../widgets/custom_image_widget.dart'; +export '../theme/app_theme.dart'; diff --git a/old/lib/main.dart b/old/lib/main.dart new file mode 100644 index 0000000..dc516e4 --- /dev/null +++ b/old/lib/main.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; + +import './services/app_state_service.dart'; +import './services/supabase_service.dart'; +import './services/api_client.dart'; +import 'core/app_export.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + try { + // Initialize Supabase (required for app functionality) + try { + await SupabaseService.initialize(); + debugPrint('✅ Supabase initialized successfully'); + } catch (e) { + debugPrint('❌ Supabase initialization failed: $e'); + debugPrint(' Make sure to run with: ./scripts/run-flutter-supabase.sh'); + // Continue - app will show error UI + } + + // Initialize API Client (optional, for PostgreSQL backend) + try { + final apiBaseUrl = ApiClient.getBaseUrl(); + ApiClient.instance.initialize(baseUrl: apiBaseUrl); + debugPrint('✅ API Client initialized: $apiBaseUrl'); + } catch (e) { + debugPrint('⚠️ API Client initialization skipped: $e'); + } + + // Initialize global app state + await AppStateService().initialize(); + debugPrint('✅ App state initialized successfully'); + } catch (e) { + debugPrint('❌ Initialization failed: $e'); + // Continue running the app even if initialization fails + } + + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return Sizer( + builder: (context, orientation, deviceType) { + return AnimatedBuilder( + animation: AppStateService(), + builder: (context, child) { + return MaterialApp( + title: 'SIBU - Sistema de Transporte', + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + themeMode: ThemeMode.system, + debugShowCheckedModeBanner: false, + builder: (context, child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear(1.0), + ), + child: child!, + ); + }, + routes: AppRoutes.routes, + initialRoute: AppRoutes.splash, + ); + }, + ); + }, + ); + } +} diff --git a/old/lib/models/bus_stop_model.dart b/old/lib/models/bus_stop_model.dart new file mode 100644 index 0000000..5d8d1d1 --- /dev/null +++ b/old/lib/models/bus_stop_model.dart @@ -0,0 +1,203 @@ +class BusStopModel { + final String id; + final String name; + final double lat; + final double lng; + final String? city; + final String? address; + final String? parentId; + final String? side; + final String stopType; + final bool hasShelter; + final bool hasSeating; + final bool isAccessible; + final DateTime? createdAt; + final DateTime? updatedAt; + + // Route-specific fields (from route_stops junction table) + final int? stopOrder; + final int? travelTimeMinutes; + final bool? isPickupPoint; + final bool? isDropoffPoint; + + BusStopModel({ + required this.id, + required this.name, + required this.lat, + required this.lng, + this.city, + this.address, + this.parentId, + this.side, + this.stopType = 'regular', + this.hasShelter = false, + this.hasSeating = false, + this.isAccessible = false, + this.createdAt, + this.updatedAt, + this.stopOrder, + this.travelTimeMinutes, + this.isPickupPoint, + this.isDropoffPoint, + }); + + factory BusStopModel.fromJson(Map json) { + return BusStopModel( + id: json['id']?.toString() ?? '', + name: json['name']?.toString() ?? '', + lat: double.tryParse(json['lat']?.toString() ?? '0') ?? 0.0, + lng: double.tryParse(json['lng']?.toString() ?? '0') ?? 0.0, + city: json['city']?.toString(), + address: json['address']?.toString(), + parentId: json['parent_id']?.toString(), + side: json['side']?.toString(), + stopType: json['stop_type']?.toString() ?? 'regular', + hasShelter: json['has_shelter'] == true, + hasSeating: json['has_seating'] == true, + isAccessible: json['is_accessible'] == true, + createdAt: + json['created_at'] != null + ? DateTime.tryParse(json['created_at'].toString()) + : null, + updatedAt: + json['updated_at'] != null + ? DateTime.tryParse(json['updated_at'].toString()) + : null, + stopOrder: + json['stop_order'] != null + ? int.tryParse(json['stop_order'].toString()) + : null, + travelTimeMinutes: + json['travel_time_minutes'] != null + ? int.tryParse(json['travel_time_minutes'].toString()) + : null, + isPickupPoint: json['is_pickup_point'] == true, + isDropoffPoint: json['is_dropoff_point'] == true, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'lat': lat, + 'lng': lng, + 'city': city, + 'address': address, + 'parent_id': parentId, + 'side': side, + 'stop_type': stopType, + 'has_shelter': hasShelter, + 'has_seating': hasSeating, + 'is_accessible': isAccessible, + 'created_at': createdAt?.toIso8601String(), + 'updated_at': updatedAt?.toIso8601String(), + 'stop_order': stopOrder, + 'travel_time_minutes': travelTimeMinutes, + 'is_pickup_point': isPickupPoint, + 'is_dropoff_point': isDropoffPoint, + }; + } + + // Helper getters + String get displayName => name; + + String get fullAddress { + if (address != null && address!.isNotEmpty) { + return city != null ? '$address, $city' : address!; + } + return city ?? 'Ubicación desconocida'; + } + + String get stopTypeDisplay { + switch (stopType) { + case 'terminal': + return 'Terminal'; + case 'express_only': + return 'Solo Express'; + case 'regular': + default: + return 'Parada Regular'; + } + } + + List get amenities { + List amenityList = []; + if (hasShelter) amenityList.add('Refugio'); + if (hasSeating) amenityList.add('Asientos'); + if (isAccessible) amenityList.add('Accesible'); + return amenityList; + } + + String get amenitiesText { + final amenityList = amenities; + if (amenityList.isEmpty) return 'Sin servicios especiales'; + return amenityList.join(', '); + } + + bool get isTerminal => stopType == 'terminal'; + bool get isExpressOnly => stopType == 'express_only'; + + String get travelTimeText { + if (travelTimeMinutes != null && travelTimeMinutes! > 0) { + return '${travelTimeMinutes} min'; + } + return 'N/A'; + } + + BusStopModel copyWith({ + String? id, + String? name, + double? lat, + double? lng, + String? city, + String? address, + String? parentId, + String? side, + String? stopType, + bool? hasShelter, + bool? hasSeating, + bool? isAccessible, + DateTime? createdAt, + DateTime? updatedAt, + int? stopOrder, + int? travelTimeMinutes, + bool? isPickupPoint, + bool? isDropoffPoint, + }) { + return BusStopModel( + id: id ?? this.id, + name: name ?? this.name, + lat: lat ?? this.lat, + lng: lng ?? this.lng, + city: city ?? this.city, + address: address ?? this.address, + parentId: parentId ?? this.parentId, + side: side ?? this.side, + stopType: stopType ?? this.stopType, + hasShelter: hasShelter ?? this.hasShelter, + hasSeating: hasSeating ?? this.hasSeating, + isAccessible: isAccessible ?? this.isAccessible, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + stopOrder: stopOrder ?? this.stopOrder, + travelTimeMinutes: travelTimeMinutes ?? this.travelTimeMinutes, + isPickupPoint: isPickupPoint ?? this.isPickupPoint, + isDropoffPoint: isDropoffPoint ?? this.isDropoffPoint, + ); + } + + @override + String toString() { + return 'BusStopModel(id: $id, name: $name, city: $city, stopType: $stopType)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is BusStopModel && other.id == id; + } + + @override + int get hashCode => id.hashCode; +} diff --git a/old/lib/models/coupon_model.dart b/old/lib/models/coupon_model.dart new file mode 100644 index 0000000..2516add --- /dev/null +++ b/old/lib/models/coupon_model.dart @@ -0,0 +1,113 @@ +class CouponModel { + final String id; + final String businessName; + final String title; + final String description; + final DateTime? validUntil; + final String? imageUrl; + final String category; + final bool isActive; + final DateTime createdAt; + + CouponModel({ + required this.id, + required this.businessName, + required this.title, + required this.description, + this.validUntil, + this.imageUrl, + required this.category, + required this.isActive, + required this.createdAt, + }); + + factory CouponModel.fromMap(Map map) { + return CouponModel( + id: map['id'] as String, + businessName: map['business_name'] as String, + title: map['title'] as String, + description: map['description'] as String? ?? '', + validUntil: map['valid_until'] != null + ? DateTime.parse(map['valid_until']) + : null, + imageUrl: map['image_url'] as String?, + category: map['category'] as String, + isActive: map['is_active'] as bool? ?? true, + createdAt: DateTime.parse(map['created_at']), + ); + } + + Map toMap() { + return { + 'id': id, + 'business_name': businessName, + 'title': title, + 'description': description, + 'valid_until': validUntil?.toIso8601String(), + 'image_url': imageUrl, + 'category': category, + 'is_active': isActive, + 'created_at': createdAt.toIso8601String(), + }; + } + + bool get isExpired { + if (validUntil == null) return false; + return DateTime.now().isAfter(validUntil!); + } + + bool get isExpiringSoon { + if (validUntil == null) return false; + final now = DateTime.now(); + final difference = validUntil!.difference(now).inDays; + return difference <= 3 && difference >= 0; + } + + String get categoryDisplayName { + switch (category.toLowerCase()) { + case 'restaurantes': + return 'Restaurantes'; + case 'tiendas': + return 'Tiendas'; + case 'servicios': + return 'Servicios'; + case 'entretenimiento': + return 'Entretenimiento'; + case 'salud': + return 'Salud'; + case 'belleza': + return 'Belleza'; + default: + return 'Otros'; + } + } + + String get validUntilFormatted { + if (validUntil == null) return 'Sin fecha de vencimiento'; + return '${validUntil!.day.toString().padLeft(2, '0')}/${validUntil!.month.toString().padLeft(2, '0')}/${validUntil!.year}'; + } + + CouponModel copyWith({ + String? id, + String? businessName, + String? title, + String? description, + DateTime? validUntil, + String? imageUrl, + String? category, + bool? isActive, + DateTime? createdAt, + }) { + return CouponModel( + id: id ?? this.id, + businessName: businessName ?? this.businessName, + title: title ?? this.title, + description: description ?? this.description, + validUntil: validUntil ?? this.validUntil, + imageUrl: imageUrl ?? this.imageUrl, + category: category ?? this.category, + isActive: isActive ?? this.isActive, + createdAt: createdAt ?? this.createdAt, + ); + } +} diff --git a/old/lib/models/route_model.dart b/old/lib/models/route_model.dart new file mode 100644 index 0000000..df8f174 --- /dev/null +++ b/old/lib/models/route_model.dart @@ -0,0 +1,154 @@ +class RouteModel { + final String id; + final String name; + final String? description; + final String color; + final String direction; + final String? originCity; + final String? destinationCity; + final double? distanceKm; + final int? estimatedDurationMinutes; + final String status; + final DateTime? createdAt; + final DateTime? updatedAt; + + RouteModel({ + required this.id, + required this.name, + this.description, + required this.color, + required this.direction, + this.originCity, + this.destinationCity, + this.distanceKm, + this.estimatedDurationMinutes, + this.status = 'active', + this.createdAt, + this.updatedAt, + }); + + factory RouteModel.fromJson(Map json) { + return RouteModel( + id: json['id']?.toString() ?? '', + name: json['name']?.toString() ?? '', + description: json['description']?.toString(), + color: json['color']?.toString() ?? '#FEE715', + direction: json['direction']?.toString() ?? 'outbound', + originCity: json['origin_city']?.toString(), + destinationCity: json['destination_city']?.toString(), + distanceKm: json['distance_km'] != null + ? double.tryParse(json['distance_km'].toString()) + : null, + estimatedDurationMinutes: json['estimated_duration_minutes'] != null + ? int.tryParse(json['estimated_duration_minutes'].toString()) + : null, + status: json['status']?.toString() ?? 'active', + createdAt: json['created_at'] != null + ? DateTime.tryParse(json['created_at'].toString()) + : null, + updatedAt: json['updated_at'] != null + ? DateTime.tryParse(json['updated_at'].toString()) + : null, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'description': description, + 'color': color, + 'direction': direction, + 'origin_city': originCity, + 'destination_city': destinationCity, + 'distance_km': distanceKm, + 'estimated_duration_minutes': estimatedDurationMinutes, + 'status': status, + 'created_at': createdAt?.toIso8601String(), + 'updated_at': updatedAt?.toIso8601String(), + }; + } + + // Helper getters + String get displayName { + if (name.isNotEmpty) return name; + final od = [originCity, destinationCity] + .where((e) => e != null && e.trim().isNotEmpty) + .map((e) => e!.trim()) + .join(' – '); + return od.isNotEmpty ? od : 'Route'; + } + + String get routeDescription { + if (description != null && description!.isNotEmpty) { + return description!; + } + return 'Ruta $displayName'; + } + + String get durationText { + if (estimatedDurationMinutes != null) { + if (estimatedDurationMinutes! >= 60) { + final hours = estimatedDurationMinutes! ~/ 60; + final minutes = estimatedDurationMinutes! % 60; + return minutes > 0 ? '${hours}h ${minutes}min' : '${hours}h'; + } + return '${estimatedDurationMinutes}min'; + } + return 'N/A'; + } + + String get distanceText { + if (distanceKm != null) { + return '${distanceKm!.toStringAsFixed(1)} km'; + } + return 'N/A'; + } + + bool get isActive => status == 'active'; + + RouteModel copyWith({ + String? id, + String? name, + String? description, + String? color, + String? direction, + String? originCity, + String? destinationCity, + double? distanceKm, + int? estimatedDurationMinutes, + String? status, + DateTime? createdAt, + DateTime? updatedAt, + }) { + return RouteModel( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + color: color ?? this.color, + direction: direction ?? this.direction, + originCity: originCity ?? this.originCity, + destinationCity: destinationCity ?? this.destinationCity, + distanceKm: distanceKm ?? this.distanceKm, + estimatedDurationMinutes: + estimatedDurationMinutes ?? this.estimatedDurationMinutes, + status: status ?? this.status, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + } + + @override + String toString() { + return 'RouteModel(id: $id, name: $name, direction: $direction, status: $status)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is RouteModel && other.id == id; + } + + @override + int get hashCode => id.hashCode; +} diff --git a/old/lib/models/route_stop_model.dart b/old/lib/models/route_stop_model.dart new file mode 100644 index 0000000..4cce970 --- /dev/null +++ b/old/lib/models/route_stop_model.dart @@ -0,0 +1,72 @@ +class RouteStopModel { + final String id; + final String routeId; + final String stopId; + final int stopOrder; + final int? travelTimeMinutes; + final bool isPickupPoint; + final bool isDropoffPoint; + final DateTime createdAt; + + // Populated from joined data + final String? stopName; + final double? latitude; + final double? longitude; + final String? city; + + RouteStopModel({ + required this.id, + required this.routeId, + required this.stopId, + required this.stopOrder, + this.travelTimeMinutes, + required this.isPickupPoint, + required this.isDropoffPoint, + required this.createdAt, + this.stopName, + this.latitude, + this.longitude, + this.city, + }); + + factory RouteStopModel.fromJson(Map json) { + return RouteStopModel( + id: json['id'] as String, + routeId: json['route_id'] as String, + stopId: json['stop_id'] as String, + stopOrder: json['stop_order'] as int, + travelTimeMinutes: json['travel_time_minutes'] as int?, + isPickupPoint: json['is_pickup_point'] as bool, + isDropoffPoint: json['is_dropoff_point'] as bool, + createdAt: DateTime.parse(json['created_at'] as String), + stopName: json['stop_name'] as String?, + latitude: json['latitude']?.toDouble(), + longitude: json['longitude']?.toDouble(), + city: json['city'] as String?, + ); + } + + Map toJson() { + return { + 'id': id, + 'route_id': routeId, + 'stop_id': stopId, + 'stop_order': stopOrder, + 'travel_time_minutes': travelTimeMinutes, + 'is_pickup_point': isPickupPoint, + 'is_dropoff_point': isDropoffPoint, + 'created_at': createdAt.toIso8601String(), + if (stopName != null) 'stop_name': stopName, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (city != null) 'city': city, + }; + } + + String get operationType { + if (isPickupPoint && isDropoffPoint) return 'Subida/Bajada'; + if (isPickupPoint) return 'Solo Subida'; + if (isDropoffPoint) return 'Solo Bajada'; + return 'Sin servicio'; + } +} diff --git a/old/lib/models/taxi_model.dart b/old/lib/models/taxi_model.dart new file mode 100644 index 0000000..1563574 --- /dev/null +++ b/old/lib/models/taxi_model.dart @@ -0,0 +1,140 @@ +import 'package:flutter/foundation.dart'; + +/// Model representing a taxi service with contact and location information +@immutable +class TaxiModel { + final String id; + final String name; + final String phone; + final String corregimiento; + final String shift; + final bool isActive; + final DateTime createdAt; + final DateTime updatedAt; + + const TaxiModel({ + required this.id, + required this.name, + required this.phone, + required this.corregimiento, + required this.shift, + required this.isActive, + required this.createdAt, + required this.updatedAt, + }); + + /// Create TaxiModel from Supabase JSON response + factory TaxiModel.fromJson(Map json) { + return TaxiModel( + id: json['id'] as String, + name: json['name'] as String, + phone: json['phone'] as String, + corregimiento: json['corregimiento'] as String, + shift: json['shift'] as String, + isActive: json['is_active'] as bool? ?? true, + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + ); + } + + /// Convert TaxiModel to JSON for Supabase operations + Map toJson() { + return { + 'id': id, + 'name': name, + 'phone': phone, + 'corregimiento': corregimiento, + 'shift': shift, + 'is_active': isActive, + 'created_at': createdAt.toIso8601String(), + 'updated_at': updatedAt.toIso8601String(), + }; + } + + /// Create a copy with modified properties + TaxiModel copyWith({ + String? id, + String? name, + String? phone, + String? corregimiento, + String? shift, + bool? isActive, + DateTime? createdAt, + DateTime? updatedAt, + }) { + return TaxiModel( + id: id ?? this.id, + name: name ?? this.name, + phone: phone ?? this.phone, + corregimiento: corregimiento ?? this.corregimiento, + shift: shift ?? this.shift, + isActive: isActive ?? this.isActive, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is TaxiModel && other.id == id; + } + + @override + int get hashCode => id.hashCode; + + @override + String toString() { + return 'TaxiModel(id: $id, name: $name, phone: $phone, corregimiento: $corregimiento, shift: $shift, isActive: $isActive)'; + } +} + +/// Model representing a user's favorite taxi +@immutable +class FavoriteTaxiModel { + final String id; + final String userId; + final String taxiId; + final DateTime createdAt; + + const FavoriteTaxiModel({ + required this.id, + required this.userId, + required this.taxiId, + required this.createdAt, + }); + + /// Create FavoriteTaxiModel from Supabase JSON response + factory FavoriteTaxiModel.fromJson(Map json) { + return FavoriteTaxiModel( + id: json['id'] as String, + userId: json['user_id'] as String, + taxiId: json['taxi_id'] as String, + createdAt: DateTime.parse(json['created_at'] as String), + ); + } + + /// Convert FavoriteTaxiModel to JSON for Supabase operations + Map toJson() { + return { + 'id': id, + 'user_id': userId, + 'taxi_id': taxiId, + 'created_at': createdAt.toIso8601String(), + }; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is FavoriteTaxiModel && other.id == id; + } + + @override + int get hashCode => id.hashCode; + + @override + String toString() { + return 'FavoriteTaxiModel(id: $id, userId: $userId, taxiId: $taxiId)'; + } +} diff --git a/old/lib/presentation/bus_stop_details/bus_stop_details.dart b/old/lib/presentation/bus_stop_details/bus_stop_details.dart new file mode 100644 index 0000000..6db442a --- /dev/null +++ b/old/lib/presentation/bus_stop_details/bus_stop_details.dart @@ -0,0 +1,440 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:sizer/sizer.dart'; + +import '../../core/app_export.dart'; +import './widgets/bus_route_card_widget.dart'; +import './widgets/nearby_landmarks_widget.dart'; +import './widgets/report_issue_widget.dart'; +import './widgets/stop_amenities_widget.dart'; +import './widgets/user_comments_widget.dart'; + +class BusStopDetails extends StatefulWidget { + const BusStopDetails({super.key}); + + @override + State createState() => _BusStopDetailsState(); +} + +class _BusStopDetailsState extends State { + bool _isLoading = true; + DateTime _lastUpdated = DateTime.now(); + + // Mock data for bus stop details + final Map _busStopData = { + "stopId": "BS001", + "stopName": "Parada Central Boquete", + "address": "Av. Central, frente al Parque José Domingo de Obaldía", + "coordinates": {"lat": 8.7833, "lng": -82.4333}, + "lastUpdated": "2025-10-19 18:45:00", + }; + + final List> _routesData = [ + { + "routeName": "Boquete - David", + "nextBusMinutes": 12, + "upcomingTimes": ["19:15", "19:45", "20:15", "20:45"], + "isDelayed": false, + }, + { + "routeName": "David - Boquete", + "nextBusMinutes": 25, + "upcomingTimes": ["19:30", "20:00", "20:30", "21:00"], + "isDelayed": true, + }, + { + "routeName": "Boquete - Caldera", + "nextBusMinutes": 45, + "upcomingTimes": ["19:50", "20:50", "21:50"], + "isDelayed": false, + }, + ]; + + final Map _amenitiesData = { + "hasShelter": true, + "hasBench": true, + "isAccessible": false, + "hasLighting": true, + "hasTrashCan": true, + }; + + final List> _landmarksData = [ + { + "name": "Parque José Domingo de Obaldía", + "distance": "50m", + "type": "park", + }, + { + "name": "Banco Nacional de Panamá", + "distance": "120m", + "type": "bank", + }, + { + "name": "Supermercado El Mandado", + "distance": "200m", + "type": "store", + }, + { + "name": "Hospital Regional de Boquete", + "distance": "350m", + "type": "hospital", + }, + ]; + + final List> _commentsData = [ + { + "userName": "María González", + "userAvatar": + "https://images.unsplash.com/photo-1687757660301-7aac1198ed63", + "semanticLabel": + "Profile photo of a woman with long brown hair wearing a blue blouse, smiling at the camera", + "comment": + "Muy buena parada, siempre está limpia y los buses llegan a tiempo. El refugio protege bien de la lluvia.", + "timestamp": "Hace 2 horas", + "rating": 5, + }, + { + "userName": "Carlos Rodríguez", + "userAvatar": + "https://images.unsplash.com/photo-1735651705945-64bc6d18d555", + "semanticLabel": + "Profile photo of a middle-aged man with short gray hair and glasses wearing a white shirt", + "comment": + "La parada está bien ubicada pero necesita mejor iluminación en las noches. Los horarios son confiables.", + "timestamp": "Hace 1 día", + "rating": 4, + }, + { + "userName": "Ana Morales", + "userAvatar": + "https://images.unsplash.com/photo-1722291493584-9e75986c6c5c", + "semanticLabel": + "Profile photo of a young woman with curly black hair wearing a red top, smiling outdoors", + "comment": + "Excelente ubicación cerca del parque. Los buses de la ruta Boquete-David son muy puntuales.", + "timestamp": "Hace 3 días", + "rating": 5, + }, + ]; + + @override + void initState() { + super.initState(); + _loadBusStopData(); + } + + Future _loadBusStopData() async { + // Simulate loading data + await Future.delayed(const Duration(seconds: 1)); + + if (mounted) { + setState(() { + _isLoading = false; + _lastUpdated = DateTime.now(); + }); + } + } + + Future _refreshData() async { + setState(() { + _isLoading = true; + }); + + await _loadBusStopData(); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Información actualizada'), + backgroundColor: AppTheme.successGreen, + duration: Duration(seconds: 2), + ), + ); + } + } + + void _shareStopInfo() { + HapticFeedback.lightImpact(); + + final stopName = _busStopData['stopName'] as String; + final address = _busStopData['address'] as String; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Compartiendo información de: $stopName'), + backgroundColor: AppTheme.primaryBlack, + duration: const Duration(seconds: 2), + ), + ); + } + + void _handleNotificationToggle() { + HapticFeedback.lightImpact(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Notificación configurada'), + backgroundColor: AppTheme.accentYellow, + duration: Duration(seconds: 2), + ), + ); + } + + void _handleAddComment() { + HapticFeedback.lightImpact(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Función de comentarios próximamente'), + backgroundColor: AppTheme.primaryBlack, + duration: Duration(seconds: 2), + ), + ); + } + + void _handleReportSubmitted() { + HapticFeedback.lightImpact(); + } + + String _formatLastUpdated() { + final now = DateTime.now(); + final difference = now.difference(_lastUpdated); + + if (difference.inMinutes < 1) { + return 'Actualizado hace unos segundos'; + } else if (difference.inMinutes < 60) { + return 'Actualizado hace ${difference.inMinutes} min'; + } else { + return 'Actualizado hace ${difference.inHours}h'; + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final stopName = _busStopData['stopName'] as String? ?? ''; + final address = _busStopData['address'] as String? ?? ''; + + return Scaffold( + backgroundColor: AppTheme.backgroundGray, + appBar: AppBar( + backgroundColor: theme.colorScheme.surface, + elevation: 0, + leading: GestureDetector( + onTap: () { + HapticFeedback.lightImpact(); + Navigator.pop(context); + }, + child: Container( + margin: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: theme.shadowColor.withValues(alpha: 0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Center( + child: CustomIconWidget( + iconName: 'close', + color: theme.colorScheme.onSurface, + size: 24, + ), + ), + ), + ), + actions: [ + GestureDetector( + onTap: _shareStopInfo, + child: Container( + margin: EdgeInsets.all(2.w), + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: theme.shadowColor.withValues(alpha: 0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: CustomIconWidget( + iconName: 'share', + color: theme.colorScheme.onSurface, + size: 24, + ), + ), + ), + ], + systemOverlayStyle: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: theme.brightness == Brightness.light + ? Brightness.dark + : Brightness.light, + ), + ), + body: _isLoading + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator( + color: AppTheme.accentYellow, + ), + SizedBox(height: 2.h), + Text( + 'Cargando información de la parada...', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + ], + ), + ) + : RefreshIndicator( + onRefresh: _refreshData, + color: AppTheme.accentYellow, + backgroundColor: theme.colorScheme.surface, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Stop header information + Container( + width: double.infinity, + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: theme.shadowColor.withValues(alpha: 0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + stopName, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w700, + color: theme.colorScheme.onSurface, + ), + ), + SizedBox(height: 1.h), + Row( + children: [ + CustomIconWidget( + iconName: 'location_on', + color: AppTheme.primaryBlack, + size: 16, + ), + SizedBox(width: 1.w), + Expanded( + child: Text( + address, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface + .withValues(alpha: 0.7), + ), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ), + ], + ), + SizedBox(height: 2.h), + Container( + padding: EdgeInsets.symmetric( + horizontal: 3.w, vertical: 1.h), + decoration: BoxDecoration( + color: AppTheme.successGreen + .withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CustomIconWidget( + iconName: 'access_time', + color: AppTheme.successGreen, + size: 14, + ), + SizedBox(width: 1.w), + Text( + _formatLastUpdated(), + style: theme.textTheme.labelSmall?.copyWith( + color: AppTheme.successGreen, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ), + SizedBox(height: 3.h), + + // Real-time arrival predictions + Text( + 'Llegadas en tiempo real', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), + ), + SizedBox(height: 2.h), + + // Route cards + ..._routesData + .map((routeData) => BusRouteCardWidget( + routeData: routeData, + onNotificationToggle: _handleNotificationToggle, + )) + .toList(), + + SizedBox(height: 4.h), + + // Stop amenities + StopAmenitiesWidget(amenitiesData: _amenitiesData), + + SizedBox(height: 3.h), + + // Nearby landmarks + NearbyLandmarksWidget(landmarks: _landmarksData), + + SizedBox(height: 3.h), + + // User comments section + UserCommentsWidget( + comments: _commentsData, + onAddComment: _handleAddComment, + ), + + SizedBox(height: 3.h), + + // Report issue section + ReportIssueWidget( + onReportSubmitted: _handleReportSubmitted, + ), + + SizedBox(height: 4.h), + ], + ), + ), + ), + ), + ); + } +} diff --git a/old/lib/presentation/bus_stop_details/widgets/bus_route_card_widget.dart b/old/lib/presentation/bus_stop_details/widgets/bus_route_card_widget.dart new file mode 100644 index 0000000..f7abf9a --- /dev/null +++ b/old/lib/presentation/bus_stop_details/widgets/bus_route_card_widget.dart @@ -0,0 +1,207 @@ +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../../core/app_export.dart'; + +class BusRouteCardWidget extends StatefulWidget { + final Map routeData; + final VoidCallback? onNotificationToggle; + + const BusRouteCardWidget({ + super.key, + required this.routeData, + this.onNotificationToggle, + }); + + @override + State createState() => _BusRouteCardWidgetState(); +} + +class _BusRouteCardWidgetState extends State { + bool _isNotificationEnabled = false; + + void _toggleNotification() { + setState(() { + _isNotificationEnabled = !_isNotificationEnabled; + }); + if (widget.onNotificationToggle != null) { + widget.onNotificationToggle!(); + } + } + + String _formatTime(int minutes) { + if (minutes < 60) { + return '${minutes}min'; + } else { + final hours = minutes ~/ 60; + final remainingMinutes = minutes % 60; + return remainingMinutes > 0 + ? '${hours}h ${remainingMinutes}min' + : '${hours}h'; + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final routeName = widget.routeData['routeName'] as String? ?? ''; + final nextBusMinutes = widget.routeData['nextBusMinutes'] as int? ?? 0; + final upcomingTimes = + (widget.routeData['upcomingTimes'] as List?)?.cast() ?? []; + final isDelayed = widget.routeData['isDelayed'] as bool? ?? false; + + return Container( + margin: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: theme.shadowColor.withValues(alpha: 0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Padding( + padding: EdgeInsets.all(4.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Route header with notification toggle + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + routeName, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), + overflow: TextOverflow.ellipsis, + ), + if (isDelayed) ...[ + SizedBox(height: 0.5.h), + Container( + padding: EdgeInsets.symmetric( + horizontal: 2.w, vertical: 0.5.h), + decoration: BoxDecoration( + color: + AppTheme.warningOrange.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + 'Retraso reportado', + style: theme.textTheme.labelSmall?.copyWith( + color: AppTheme.warningOrange, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ], + ), + ), + GestureDetector( + onTap: _toggleNotification, + child: Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: _isNotificationEnabled + ? AppTheme.accentYellow.withValues(alpha: 0.1) + : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + child: CustomIconWidget( + iconName: _isNotificationEnabled + ? 'notifications' + : 'notifications_none', + color: _isNotificationEnabled + ? AppTheme.accentYellow + : theme.colorScheme.onSurface.withValues(alpha: 0.6), + size: 24, + ), + ), + ), + ], + ), + SizedBox(height: 3.h), + // Next bus countdown + Container( + width: double.infinity, + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: AppTheme.accentYellow.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: AppTheme.accentYellow.withValues(alpha: 0.3), + width: 1, + ), + ), + child: Column( + children: [ + Text( + 'Próximo bus en:', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + SizedBox(height: 1.h), + Text( + _formatTime(nextBusMinutes), + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w700, + color: AppTheme.primaryBlack, + ), + ), + ], + ), + ), + // Upcoming times + if (upcomingTimes.isNotEmpty) ...[ + SizedBox(height: 3.h), + Text( + 'Próximas salidas:', + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + color: theme.colorScheme.onSurface, + ), + ), + SizedBox(height: 1.h), + Wrap( + spacing: 2.w, + runSpacing: 1.h, + children: upcomingTimes + .take(4) + .map((time) => Container( + padding: EdgeInsets.symmetric( + horizontal: 3.w, vertical: 1.h), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: theme.colorScheme.outline + .withValues(alpha: 0.3), + width: 1, + ), + ), + child: Text( + time, + style: theme.textTheme.bodySmall?.copyWith( + fontWeight: FontWeight.w500, + color: theme.colorScheme.onSurface, + ), + ), + )) + .toList(), + ), + ], + ], + ), + ), + ); + } +} diff --git a/old/lib/presentation/bus_stop_details/widgets/nearby_landmarks_widget.dart b/old/lib/presentation/bus_stop_details/widgets/nearby_landmarks_widget.dart new file mode 100644 index 0000000..2584ad7 --- /dev/null +++ b/old/lib/presentation/bus_stop_details/widgets/nearby_landmarks_widget.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../../core/app_export.dart'; + +class NearbyLandmarksWidget extends StatelessWidget { + final List> landmarks; + + const NearbyLandmarksWidget({ + super.key, + required this.landmarks, + }); + + Widget _buildLandmarkItem( + BuildContext context, Map landmark) { + final theme = Theme.of(context); + final name = landmark['name'] as String? ?? ''; + final distance = landmark['distance'] as String? ?? ''; + final type = landmark['type'] as String? ?? ''; + + String iconName = 'place'; + switch (type.toLowerCase()) { + case 'restaurant': + iconName = 'restaurant'; + break; + case 'hospital': + iconName = 'local_hospital'; + break; + case 'school': + iconName = 'school'; + break; + case 'bank': + iconName = 'account_balance'; + break; + case 'store': + iconName = 'store'; + break; + case 'gas_station': + iconName = 'local_gas_station'; + break; + default: + iconName = 'place'; + } + + return Container( + margin: EdgeInsets.only(bottom: 2.h), + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: theme.colorScheme.surface.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: theme.colorScheme.outline.withValues(alpha: 0.2), + width: 1, + ), + ), + child: Row( + children: [ + Container( + width: 10.w, + height: 5.h, + decoration: BoxDecoration( + color: AppTheme.primaryBlack.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Center( + child: CustomIconWidget( + iconName: iconName, + color: AppTheme.primaryBlack, + size: 20, + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + color: theme.colorScheme.onSurface, + ), + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 0.5.h), + Text( + distance, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + ], + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + if (landmarks.isEmpty) { + return Container( + width: double.infinity, + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: theme.shadowColor.withValues(alpha: 0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + CustomIconWidget( + iconName: 'location_off', + color: theme.colorScheme.onSurface.withValues(alpha: 0.4), + size: 32, + ), + SizedBox(height: 2.h), + Text( + 'No hay puntos de referencia cercanos', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + return Container( + width: double.infinity, + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: theme.shadowColor.withValues(alpha: 0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Puntos de referencia cercanos', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), + ), + SizedBox(height: 3.h), + ...landmarks + .map((landmark) => _buildLandmarkItem(context, landmark)) + .toList(), + ], + ), + ); + } +} diff --git a/old/lib/presentation/bus_stop_details/widgets/report_issue_widget.dart b/old/lib/presentation/bus_stop_details/widgets/report_issue_widget.dart new file mode 100644 index 0000000..a51c244 --- /dev/null +++ b/old/lib/presentation/bus_stop_details/widgets/report_issue_widget.dart @@ -0,0 +1,396 @@ +import 'package:camera/camera.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../../core/app_export.dart'; + +class ReportIssueWidget extends StatefulWidget { + final VoidCallback? onReportSubmitted; + + const ReportIssueWidget({ + super.key, + this.onReportSubmitted, + }); + + @override + State createState() => _ReportIssueWidgetState(); +} + +class _ReportIssueWidgetState extends State { + final TextEditingController _issueController = TextEditingController(); + String _selectedIssueType = 'Limpieza'; + XFile? _capturedImage; + CameraController? _cameraController; + List _cameras = []; + bool _isCameraInitialized = false; + bool _isSubmitting = false; + + final List _issueTypes = [ + 'Limpieza', + 'Daños en la estructura', + 'Falta de iluminación', + 'Problemas de accesibilidad', + 'Vandalismo', + 'Otro', + ]; + + @override + void initState() { + super.initState(); + _initializeCamera(); + } + + @override + void dispose() { + _issueController.dispose(); + _cameraController?.dispose(); + super.dispose(); + } + + Future _requestCameraPermission() async { + if (kIsWeb) return true; + return (await Permission.camera.request()).isGranted; + } + + Future _initializeCamera() async { + try { + if (!await _requestCameraPermission()) return; + + _cameras = await availableCameras(); + if (_cameras.isEmpty) return; + + final camera = kIsWeb + ? _cameras.firstWhere( + (c) => c.lensDirection == CameraLensDirection.front, + orElse: () => _cameras.first) + : _cameras.firstWhere( + (c) => c.lensDirection == CameraLensDirection.back, + orElse: () => _cameras.first); + + _cameraController = CameraController( + camera, kIsWeb ? ResolutionPreset.medium : ResolutionPreset.high); + await _cameraController!.initialize(); + + if (!kIsWeb) { + try { + await _cameraController!.setFocusMode(FocusMode.auto); + await _cameraController!.setFlashMode(FlashMode.auto); + } catch (e) {} + } + + if (mounted) { + setState(() { + _isCameraInitialized = true; + }); + } + } catch (e) { + // Silent fail - camera not available + } + } + + Future _capturePhoto() async { + if (_cameraController == null || !_cameraController!.value.isInitialized) + return; + + try { + final XFile photo = await _cameraController!.takePicture(); + setState(() { + _capturedImage = photo; + }); + } catch (e) { + // Silent fail + } + } + + Future _pickImageFromGallery() async { + try { + final ImagePicker picker = ImagePicker(); + final XFile? image = await picker.pickImage(source: ImageSource.gallery); + if (image != null) { + setState(() { + _capturedImage = image; + }); + } + } catch (e) { + // Silent fail + } + } + + Future _submitReport() async { + if (_issueController.text.trim().isEmpty) return; + + setState(() { + _isSubmitting = true; + }); + + // Simulate report submission + await Future.delayed(const Duration(seconds: 2)); + + if (mounted) { + setState(() { + _isSubmitting = false; + _issueController.clear(); + _capturedImage = null; + _selectedIssueType = 'Limpieza'; + }); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Reporte enviado exitosamente'), + backgroundColor: AppTheme.successGreen, + ), + ); + + if (widget.onReportSubmitted != null) { + widget.onReportSubmitted!(); + } + } + } + + void _showReportDialog() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => Container( + height: 85.h, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Padding( + padding: EdgeInsets.all(4.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Handle bar + Center( + child: Container( + width: 12.w, + height: 0.5.h, + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + SizedBox(height: 3.h), + // Header + Row( + children: [ + Expanded( + child: Text( + 'Reportar problema', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child: CustomIconWidget( + iconName: 'close', + color: Theme.of(context).colorScheme.onSurface, + size: 24, + ), + ), + ], + ), + SizedBox(height: 4.h), + // Issue type dropdown + Text( + 'Tipo de problema', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 1.h), + Container( + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 4.w), + decoration: BoxDecoration( + border: + Border.all(color: Theme.of(context).colorScheme.outline), + borderRadius: BorderRadius.circular(8), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: _selectedIssueType, + isExpanded: true, + items: _issueTypes + .map((type) => DropdownMenuItem( + value: type, + child: Text(type), + )) + .toList(), + onChanged: (value) { + if (value != null) { + setState(() { + _selectedIssueType = value; + }); + } + }, + ), + ), + ), + SizedBox(height: 3.h), + // Description field + Text( + 'Descripción del problema', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 1.h), + TextField( + controller: _issueController, + maxLines: 4, + decoration: const InputDecoration( + hintText: + 'Describe el problema que encontraste en esta parada...', + ), + ), + SizedBox(height: 3.h), + // Photo section + Text( + 'Agregar foto (opcional)', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 1.h), + if (_capturedImage != null) ...[ + Container( + width: double.infinity, + height: 20.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Theme.of(context).colorScheme.outline), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: kIsWeb + ? Image.network(_capturedImage!.path, fit: BoxFit.cover) + : Image.network(_capturedImage!.path, + fit: BoxFit.cover), + ), + ), + SizedBox(height: 2.h), + ], + Row( + children: [ + if (_isCameraInitialized) ...[ + Expanded( + child: ElevatedButton.icon( + onPressed: _capturePhoto, + icon: CustomIconWidget( + iconName: 'camera_alt', + color: AppTheme.primaryBlack, + size: 20, + ), + label: const Text('Tomar foto'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.accentYellow, + foregroundColor: AppTheme.primaryBlack, + ), + ), + ), + SizedBox(width: 2.w), + ], + Expanded( + child: OutlinedButton.icon( + onPressed: _pickImageFromGallery, + icon: CustomIconWidget( + iconName: 'photo_library', + color: Theme.of(context).colorScheme.primary, + size: 20, + ), + label: const Text('Galería'), + ), + ), + ], + ), + const Spacer(), + // Submit button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _isSubmitting ? null : _submitReport, + child: _isSubmitting + ? const CircularProgressIndicator() + : const Text('Enviar reporte'), + ), + ), + SizedBox(height: 2.h), + ], + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + width: double.infinity, + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: theme.shadowColor.withValues(alpha: 0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Reportar problema', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), + ), + SizedBox(height: 2.h), + Text( + '¿Encontraste algún problema en esta parada? Ayúdanos a mejorar reportándolo.', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + SizedBox(height: 3.h), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _showReportDialog, + icon: CustomIconWidget( + iconName: 'report_problem', + color: AppTheme.primaryBlack, + size: 20, + ), + label: const Text('Reportar problema'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.warningOrange, + foregroundColor: AppTheme.primaryBlack, + padding: EdgeInsets.symmetric(vertical: 3.h), + ), + ), + ), + ], + ), + ); + } +} diff --git a/old/lib/presentation/bus_stop_details/widgets/stop_amenities_widget.dart b/old/lib/presentation/bus_stop_details/widgets/stop_amenities_widget.dart new file mode 100644 index 0000000..ab96a82 --- /dev/null +++ b/old/lib/presentation/bus_stop_details/widgets/stop_amenities_widget.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../../core/app_export.dart'; + +class StopAmenitiesWidget extends StatelessWidget { + final Map amenitiesData; + + const StopAmenitiesWidget({ + super.key, + required this.amenitiesData, + }); + + Widget _buildAmenityItem( + BuildContext context, String iconName, String label, bool isAvailable) { + final theme = Theme.of(context); + + return Column( + children: [ + Container( + width: 12.w, + height: 6.h, + decoration: BoxDecoration( + color: isAvailable + ? AppTheme.successGreen.withValues(alpha: 0.1) + : theme.colorScheme.surface.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isAvailable + ? AppTheme.successGreen.withValues(alpha: 0.3) + : theme.colorScheme.outline.withValues(alpha: 0.3), + width: 1, + ), + ), + child: Center( + child: CustomIconWidget( + iconName: iconName, + color: isAvailable + ? AppTheme.successGreen + : theme.colorScheme.onSurface.withValues(alpha: 0.4), + size: 24, + ), + ), + ), + SizedBox(height: 1.h), + Text( + label, + style: theme.textTheme.labelSmall?.copyWith( + color: isAvailable + ? theme.colorScheme.onSurface + : theme.colorScheme.onSurface.withValues(alpha: 0.5), + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final hasShelter = amenitiesData['hasShelter'] as bool? ?? false; + final hasBench = amenitiesData['hasBench'] as bool? ?? false; + final isAccessible = amenitiesData['isAccessible'] as bool? ?? false; + final hasLighting = amenitiesData['hasLighting'] as bool? ?? false; + final hasTrashCan = amenitiesData['hasTrashCan'] as bool? ?? false; + + return Container( + width: double.infinity, + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: theme.shadowColor.withValues(alpha: 0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Comodidades de la parada', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), + ), + SizedBox(height: 3.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: _buildAmenityItem( + context, + 'home', + 'Refugio', + hasShelter, + ), + ), + Expanded( + child: _buildAmenityItem( + context, + 'chair', + 'Asiento', + hasBench, + ), + ), + Expanded( + child: _buildAmenityItem( + context, + 'accessible', + 'Accesible', + isAccessible, + ), + ), + Expanded( + child: _buildAmenityItem( + context, + 'lightbulb', + 'Iluminación', + hasLighting, + ), + ), + Expanded( + child: _buildAmenityItem( + context, + 'delete', + 'Basura', + hasTrashCan, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/old/lib/presentation/bus_stop_details/widgets/user_comments_widget.dart b/old/lib/presentation/bus_stop_details/widgets/user_comments_widget.dart new file mode 100644 index 0000000..4aa0bba --- /dev/null +++ b/old/lib/presentation/bus_stop_details/widgets/user_comments_widget.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../../core/app_export.dart'; + +class UserCommentsWidget extends StatefulWidget { + final List> comments; + final VoidCallback? onAddComment; + + const UserCommentsWidget({ + super.key, + required this.comments, + this.onAddComment, + }); + + @override + State createState() => _UserCommentsWidgetState(); +} + +class _UserCommentsWidgetState extends State { + bool _isExpanded = false; + + Widget _buildCommentItem(BuildContext context, Map comment) { + final theme = Theme.of(context); + final userName = comment['userName'] as String? ?? ''; + final userAvatar = comment['userAvatar'] as String? ?? ''; + final semanticLabel = comment['semanticLabel'] as String? ?? ''; + final commentText = comment['comment'] as String? ?? ''; + final timestamp = comment['timestamp'] as String? ?? ''; + final rating = comment['rating'] as int? ?? 0; + + return Container( + margin: EdgeInsets.only(bottom: 3.h), + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: theme.colorScheme.surface.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: theme.colorScheme.outline.withValues(alpha: 0.2), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // User info and rating + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: CustomImageWidget( + imageUrl: userAvatar, + width: 40, + height: 40, + fit: BoxFit.cover, + semanticLabel: semanticLabel, + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + userName, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), + overflow: TextOverflow.ellipsis, + ), + Text( + timestamp, + style: theme.textTheme.bodySmall?.copyWith( + color: + theme.colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + ], + ), + ), + // Rating stars + Row( + children: List.generate( + 5, + (index) => CustomIconWidget( + iconName: index < rating ? 'star' : 'star_border', + color: index < rating + ? AppTheme.accentYellow + : theme.colorScheme.onSurface + .withValues(alpha: 0.3), + size: 16, + )), + ), + ], + ), + SizedBox(height: 2.h), + // Comment text + Text( + commentText, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface, + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + width: double.infinity, + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: theme.shadowColor.withValues(alpha: 0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header with expand/collapse + GestureDetector( + onTap: () { + setState(() { + _isExpanded = !_isExpanded; + }); + }, + child: Row( + children: [ + Expanded( + child: Text( + 'Comentarios de usuarios (${widget.comments.length})', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), + ), + ), + CustomIconWidget( + iconName: _isExpanded ? 'expand_less' : 'expand_more', + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + size: 24, + ), + ], + ), + ), + + if (_isExpanded) ...[ + SizedBox(height: 3.h), + // Add comment button + GestureDetector( + onTap: widget.onAddComment, + child: Container( + width: double.infinity, + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: AppTheme.accentYellow.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: AppTheme.accentYellow.withValues(alpha: 0.3), + width: 1, + ), + ), + child: Row( + children: [ + CustomIconWidget( + iconName: 'add_comment', + color: AppTheme.primaryBlack, + size: 20, + ), + SizedBox(width: 2.w), + Text( + 'Agregar comentario', + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + color: AppTheme.primaryBlack, + ), + ), + ], + ), + ), + ), + SizedBox(height: 3.h), + // Comments list + if (widget.comments.isEmpty) + Container( + width: double.infinity, + padding: EdgeInsets.all(6.w), + child: Column( + children: [ + CustomIconWidget( + iconName: 'comment', + color: theme.colorScheme.onSurface.withValues(alpha: 0.4), + size: 32, + ), + SizedBox(height: 2.h), + Text( + 'Aún no hay comentarios', + style: theme.textTheme.bodyMedium?.copyWith( + color: + theme.colorScheme.onSurface.withValues(alpha: 0.6), + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 1.h), + Text( + '¡Sé el primero en compartir tu experiencia!', + style: theme.textTheme.bodySmall?.copyWith( + color: + theme.colorScheme.onSurface.withValues(alpha: 0.5), + ), + textAlign: TextAlign.center, + ), + ], + ), + ) + else + ...widget.comments + .map((comment) => _buildCommentItem(context, comment)) + .toList(), + ], + ], + ), + ); + } +} diff --git a/old/lib/presentation/coupons_screen/coupons_screen.dart b/old/lib/presentation/coupons_screen/coupons_screen.dart new file mode 100644 index 0000000..e59b18d --- /dev/null +++ b/old/lib/presentation/coupons_screen/coupons_screen.dart @@ -0,0 +1,256 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:sizer/sizer.dart'; + +import '../../core/app_export.dart'; +import '../../models/coupon_model.dart'; +import '../../services/coupon_service.dart'; +import '../../theme/app_theme.dart'; +import '../../widgets/custom_bottom_bar.dart'; +import './widgets/category_filter_chips.dart'; +import './widgets/coupon_card_widget.dart'; +import './widgets/coupon_detail_modal.dart'; +import './widgets/empty_state_widget.dart'; +import './widgets/sort_dropdown.dart'; + +class CouponsScreen extends StatefulWidget { + const CouponsScreen({super.key}); + + @override + State createState() => _CouponsScreenState(); +} + +class _CouponsScreenState extends State { + final ScrollController _scrollController = ScrollController(); + + bool _isLoading = true; + bool _isRefreshing = false; + String _selectedCategory = 'Todos'; + String _selectedSort = 'Más recientes'; + List _coupons = []; + + @override + void initState() { + super.initState(); + _loadCoupons(); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + /// Load coupons with silent error handling - no user error messages + Future _loadCoupons() async { + try { + setState(() { + _isLoading = true; + }); + + final coupons = await CouponService.getCoupons( + selectedCategory: _selectedCategory, + sort: _selectedSort, + ); + + setState(() { + _coupons = coupons; + _isLoading = false; + }); + } catch (e) { + // Silent failure - show empty state but don't show error to user + setState(() { + _coupons = []; + _isLoading = false; + }); + } + } + + /// Refresh coupons with silent error handling + Future _refreshCoupons() async { + setState(() { + _isRefreshing = true; + }); + + HapticFeedback.lightImpact(); + + try { + final coupons = await CouponService.getCoupons( + selectedCategory: _selectedCategory, + sort: _selectedSort, + ); + + setState(() { + _coupons = coupons; + }); + } catch (e) { + // Silent failure - keep existing coupons, don't show error + } finally { + setState(() { + _isRefreshing = false; + }); + } + } + + /// Handle category change and auto-refresh data + void _onCategoryChanged(String category) { + if (_selectedCategory != category) { + setState(() { + _selectedCategory = category; + }); + _loadCoupons(); // Automatically refresh when filters change + } + } + + /// Handle sort change and auto-refresh data + void _onSortChanged(String sort) { + if (_selectedSort != sort) { + setState(() { + _selectedSort = sort; + }); + _loadCoupons(); // Automatically refresh when sorting changes + } + } + + void _showCouponDetail(CouponModel coupon) { + HapticFeedback.lightImpact(); + showDialog( + context: context, + builder: (context) => CouponDetailModal(coupon: coupon), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, + appBar: AppBar( + title: Text( + 'Cupones', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + backgroundColor: theme.colorScheme.surface, + elevation: 0, + ), + body: Column( + children: [ + // Category Filter Chips (Spanish UI) + CategoryFilterChips( + selectedCategory: _selectedCategory, + onCategorySelected: _onCategoryChanged, + ), + // Sort Dropdown (Spanish UI) + Container( + padding: EdgeInsets.symmetric(vertical: 1.h), + color: theme.colorScheme.surface, + child: SortDropdown( + selectedSort: _selectedSort, + onSortChanged: _onSortChanged, + ), + ), + // Content + Expanded(child: _buildContent()), + ], + ), + bottomNavigationBar: CustomBottomBar( + currentIndex: 2, + onTap: (index) { + switch (index) { + case 0: + Navigator.pushReplacementNamed(context, '/map-screen'); + break; + case 1: + Navigator.pushReplacementNamed(context, '/schedules-screen'); + break; + case 2: + // Already on coupons screen + break; + } + }, + ), + ); + } + + Widget _buildContent() { + final theme = Theme.of(context); + + if (_isLoading) { + return Center( + child: CircularProgressIndicator(color: AppTheme.accentYellow), + ); + } + + // No error states shown to user - only empty states + if (_coupons.isEmpty) { + final isFiltered = _selectedCategory != 'Todos'; + return EmptyStateWidget( + title: + isFiltered + ? 'No hay cupones disponibles para esta categoría.' + : 'Aún no hay cupones registrados.', + subtitle: + isFiltered + ? 'Intenta seleccionando otra categoría' + : 'Vuelve pronto para ver nuevas ofertas', + actionText: isFiltered ? 'Ver todos' : null, + onActionPressed: isFiltered ? () => _onCategoryChanged('Todos') : null, + ); + } + + return RefreshIndicator( + onRefresh: _refreshCoupons, + color: AppTheme.accentYellow, + backgroundColor: theme.colorScheme.surface, + child: GridView.builder( + controller: _scrollController, + padding: EdgeInsets.all(4.w), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: _getGridCrossAxisCount(), + crossAxisSpacing: 4.w, + mainAxisSpacing: 4.w, + childAspectRatio: 0.75, + ), + itemCount: _coupons.length + (_isRefreshing ? 2 : 0), + itemBuilder: (context, index) { + if (index >= _coupons.length) { + return Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: theme.colorScheme.surface, + ), + child: Center( + child: CircularProgressIndicator( + color: AppTheme.accentYellow, + strokeWidth: 2, + ), + ), + ), + ); + } + + final coupon = _coupons[index]; + return CouponCardWidget( + coupon: coupon, + onTap: () => _showCouponDetail(coupon), + ); + }, + ), + ); + } + + int _getGridCrossAxisCount() { + if (MediaQuery.of(context).size.width > 768) { + return 3; // Tablet + } + return 2; // Phone + } +} diff --git a/old/lib/presentation/coupons_screen/widgets/category_filter_chips.dart b/old/lib/presentation/coupons_screen/widgets/category_filter_chips.dart new file mode 100644 index 0000000..ca18bc0 --- /dev/null +++ b/old/lib/presentation/coupons_screen/widgets/category_filter_chips.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../core/app_export.dart'; +import '../../../services/coupon_service.dart'; +import '../../../theme/app_theme.dart'; + +class CategoryFilterChips extends StatelessWidget { + final String selectedCategory; + final ValueChanged onCategorySelected; + + const CategoryFilterChips({ + super.key, + required this.selectedCategory, + required this.onCategorySelected, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final categories = CouponService.getCategoryOptions(); + + return Container( + height: 6.h, + padding: EdgeInsets.symmetric(vertical: 1.h), + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: 4.w), + itemCount: categories.length, + separatorBuilder: (context, index) => SizedBox(width: 2.w), + itemBuilder: (context, index) { + final category = categories[index]; + final isSelected = selectedCategory == category; + + return GestureDetector( + onTap: () { + HapticFeedback.lightImpact(); + onCategorySelected(category); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 1.h, + ), + decoration: BoxDecoration( + color: isSelected + ? AppTheme.accentYellow + : theme.colorScheme.surface, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isSelected + ? AppTheme.accentYellow + : theme.colorScheme.outline, + width: 1, + ), + ), + child: Text( + category, + style: theme.textTheme.labelMedium?.copyWith( + color: isSelected + ? AppTheme.primaryBlack + : theme.colorScheme.onSurface, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/old/lib/presentation/coupons_screen/widgets/coupon_card_widget.dart b/old/lib/presentation/coupons_screen/widgets/coupon_card_widget.dart new file mode 100644 index 0000000..6815201 --- /dev/null +++ b/old/lib/presentation/coupons_screen/widgets/coupon_card_widget.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../core/app_export.dart'; +import '../../../models/coupon_model.dart'; + +class CouponCardWidget extends StatelessWidget { + final CouponModel coupon; + final VoidCallback onTap; + + const CouponCardWidget({ + super.key, + required this.coupon, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: InkWell( + onTap: () { + HapticFeedback.lightImpact(); + onTap(); + }, + borderRadius: BorderRadius.circular(12), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: theme.colorScheme.surface, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Image Section (16:9 aspect ratio) + Expanded( + flex: 4, + child: Container( + width: double.infinity, + decoration: const BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + child: AspectRatio( + aspectRatio: 16 / 9, + child: coupon.imageUrl != null + ? CustomImageWidget( + imageUrl: coupon.imageUrl!, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + semanticLabel: 'Imagen de ${coupon.businessName}', + ) + : Container( + color: theme.colorScheme.surfaceContainerHighest, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CustomIconWidget( + iconName: 'image', + color: theme.colorScheme.onSurfaceVariant, + size: 24, + ), + SizedBox(height: 1.h), + Text( + 'Sin imagen', + style: + theme.textTheme.bodySmall?.copyWith( + color: + theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ), + ), + ), + ), + // Content Section + Expanded( + flex: 3, + child: Padding( + padding: EdgeInsets.all(3.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Business Name (bold) + Text( + coupon.businessName, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 0.5.h), + // Title (subtitle style) + Text( + coupon.title, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 0.5.h), + // Description (2 lines max, small text) + Expanded( + child: Text( + coupon.description, + style: theme.textTheme.bodySmall, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + SizedBox(height: 1.h), + // Valid until text + Text( + coupon.validUntil != null + ? 'Válido hasta: ${coupon.validUntilFormatted}' + : 'Sin fecha de vencimiento', + style: theme.textTheme.bodySmall?.copyWith( + color: coupon.isExpired + ? AppTheme.errorRed + : coupon.isExpiringSoon + ? Colors.orange + : theme.colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/old/lib/presentation/coupons_screen/widgets/coupon_detail_modal.dart b/old/lib/presentation/coupons_screen/widgets/coupon_detail_modal.dart new file mode 100644 index 0000000..7f7a850 --- /dev/null +++ b/old/lib/presentation/coupons_screen/widgets/coupon_detail_modal.dart @@ -0,0 +1,251 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../core/app_export.dart'; +import '../../../models/coupon_model.dart'; + +class CouponDetailModal extends StatelessWidget { + final CouponModel coupon; + final VoidCallback? onUseCoupon; + + const CouponDetailModal({ + super.key, + required this.coupon, + this.onUseCoupon, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: EdgeInsets.all(4.w), + child: Container( + constraints: BoxConstraints( + maxHeight: 85.h, + ), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Close Button + Align( + alignment: Alignment.topRight, + child: Padding( + padding: EdgeInsets.all(2.w), + child: IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: CustomIconWidget( + iconName: 'close', + color: theme.colorScheme.onSurface, + size: 24, + ), + ), + ), + ), + // Content + Flexible( + child: Padding( + padding: EdgeInsets.fromLTRB(6.w, 0, 6.w, 6.w), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Full Image + Container( + width: double.infinity, + height: 25.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: theme.colorScheme.surfaceContainerHighest, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: coupon.imageUrl != null + ? CustomImageWidget( + imageUrl: coupon.imageUrl!, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + semanticLabel: + 'Imagen de ${coupon.businessName}', + ) + : Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CustomIconWidget( + iconName: 'image', + color: theme.colorScheme.onSurfaceVariant, + size: 32, + ), + SizedBox(height: 1.h), + Text( + 'Sin imagen', + style: + theme.textTheme.bodyMedium?.copyWith( + color: + theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ), + SizedBox(height: 4.w), + // Business Name + Text( + coupon.businessName, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 2.w), + // Title + Text( + coupon.title, + style: theme.textTheme.titleMedium?.copyWith( + color: AppTheme.accentYellow, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 3.w), + // Description + Text( + coupon.description, + style: theme.textTheme.bodyMedium, + ), + SizedBox(height: 3.w), + // Valid Until + Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + CustomIconWidget( + iconName: 'access_time', + color: theme.colorScheme.onSurfaceVariant, + size: 20, + ), + SizedBox(width: 2.w), + Text( + coupon.validUntil != null + ? 'Válido hasta: ${coupon.validUntilFormatted}' + : 'Sin fecha de vencimiento', + style: theme.textTheme.bodyMedium?.copyWith( + color: coupon.isExpired + ? AppTheme.errorRed + : coupon.isExpiringSoon + ? Colors.orange + : theme.colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + SizedBox(height: 4.w), + // Action Buttons Row + Row( + children: [ + // Call Button (placeholder) + Expanded( + child: OutlinedButton.icon( + onPressed: () { + HapticFeedback.lightImpact(); + // TODO: Implement call functionality + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: + Text('Función de llamada próximamente'), + ), + ); + }, + icon: CustomIconWidget( + iconName: 'call', + color: theme.colorScheme.primary, + size: 20, + ), + label: const Text('Llamar'), + style: OutlinedButton.styleFrom( + foregroundColor: theme.colorScheme.primary, + side: + BorderSide(color: theme.colorScheme.outline), + ), + ), + ), + SizedBox(width: 3.w), + // WhatsApp Button (placeholder) + Expanded( + child: OutlinedButton.icon( + onPressed: () { + HapticFeedback.lightImpact(); + // TODO: Implement WhatsApp functionality + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: + Text('Función de WhatsApp próximamente'), + ), + ); + }, + icon: CustomIconWidget( + iconName: 'chat', + color: Colors.green, + size: 20, + ), + label: const Text('WhatsApp'), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.green, + side: + BorderSide(color: theme.colorScheme.outline), + ), + ), + ), + SizedBox(width: 3.w), + // Location Button (placeholder) + Expanded( + child: OutlinedButton.icon( + onPressed: () { + HapticFeedback.lightImpact(); + // TODO: Implement location functionality + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: + Text('Función de ubicación próximamente'), + ), + ); + }, + icon: CustomIconWidget( + iconName: 'location_on', + color: AppTheme.errorRed, + size: 20, + ), + label: const Text('Ubicación'), + style: OutlinedButton.styleFrom( + foregroundColor: AppTheme.errorRed, + side: + BorderSide(color: theme.colorScheme.outline), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/old/lib/presentation/coupons_screen/widgets/empty_state_widget.dart b/old/lib/presentation/coupons_screen/widgets/empty_state_widget.dart new file mode 100644 index 0000000..a015767 --- /dev/null +++ b/old/lib/presentation/coupons_screen/widgets/empty_state_widget.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../core/app_export.dart'; + +class EmptyStateWidget extends StatelessWidget { + final String title; + final String subtitle; + final String? actionText; + final VoidCallback? onActionPressed; + + const EmptyStateWidget({ + super.key, + required this.title, + required this.subtitle, + this.actionText, + this.onActionPressed, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Center( + child: Padding( + padding: EdgeInsets.all(8.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Empty state icon + Container( + padding: EdgeInsets.all(6.w), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + shape: BoxShape.circle, + ), + child: CustomIconWidget( + iconName: 'local_offer', + color: theme.colorScheme.onSurfaceVariant, + size: 48, + ), + ), + SizedBox(height: 4.h), + // Title + Text( + title, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 2.h), + // Subtitle + Text( + subtitle, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.7), + ), + textAlign: TextAlign.center, + ), + if (actionText != null && onActionPressed != null) ...[ + SizedBox(height: 4.h), + // Action button + ElevatedButton( + onPressed: onActionPressed, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.accentYellow, + foregroundColor: AppTheme.primaryBlack, + padding: EdgeInsets.symmetric( + horizontal: 8.w, + vertical: 1.5.h, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + actionText!, + style: theme.textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ], + ), + ), + ); + } +} diff --git a/old/lib/presentation/coupons_screen/widgets/filter_bottom_sheet.dart b/old/lib/presentation/coupons_screen/widgets/filter_bottom_sheet.dart new file mode 100644 index 0000000..2bbdc2e --- /dev/null +++ b/old/lib/presentation/coupons_screen/widgets/filter_bottom_sheet.dart @@ -0,0 +1,273 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../core/app_export.dart'; +import '../../../theme/app_theme.dart'; + +class FilterBottomSheet extends StatefulWidget { + final Map currentFilters; + final Function(Map) onFiltersChanged; + + const FilterBottomSheet({ + super.key, + required this.currentFilters, + required this.onFiltersChanged, + }); + + @override + State createState() => _FilterBottomSheetState(); +} + +class _FilterBottomSheetState extends State { + late Map _filters; + + final List _categories = [ + 'Todos', + 'Restaurantes', + 'Tiendas', + 'Servicios', + 'Entretenimiento', + 'Salud', + 'Belleza', + ]; + + final List _sortOptions = [ + 'Más recientes', + 'Por vencer', + 'Mayor descuento', + 'Distancia', + ]; + + @override + void initState() { + super.initState(); + _filters = Map.from(widget.currentFilters); + } + + void _applyFilters() { + HapticFeedback.lightImpact(); + widget.onFiltersChanged(_filters); + Navigator.of(context).pop(); + } + + void _resetFilters() { + HapticFeedback.lightImpact(); + setState(() { + _filters = { + 'category': 'Todos', + 'sortBy': 'Más recientes', + 'showUsed': false, + 'showExpired': false, + }; + }); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Handle bar + Container( + margin: EdgeInsets.only(top: 2.h), + width: 12.w, + height: 0.5.h, + decoration: BoxDecoration( + color: theme.colorScheme.outline, + borderRadius: BorderRadius.circular(2), + ), + ), + // Header + Padding( + padding: EdgeInsets.all(4.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Filtros', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + TextButton( + onPressed: _resetFilters, + child: Text( + 'Limpiar', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.primary, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + Divider(height: 1, color: theme.colorScheme.outline), + // Content + Flexible( + child: SingleChildScrollView( + padding: EdgeInsets.all(4.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Category Filter + Text( + 'Categoría', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 2.h), + Wrap( + spacing: 2.w, + runSpacing: 1.h, + children: _categories.map((category) { + final isSelected = _filters['category'] == category; + return FilterChip( + label: Text(category), + selected: isSelected, + onSelected: (selected) { + HapticFeedback.lightImpact(); + setState(() { + _filters['category'] = category; + }); + }, + backgroundColor: theme.colorScheme.surface, + selectedColor: + AppTheme.accentYellow.withValues(alpha: 0.2), + checkmarkColor: AppTheme.primaryBlack, + labelStyle: theme.textTheme.bodyMedium?.copyWith( + color: isSelected + ? AppTheme.primaryBlack + : theme.colorScheme.onSurface, + fontWeight: + isSelected ? FontWeight.w600 : FontWeight.w400, + ), + side: BorderSide( + color: isSelected + ? AppTheme.accentYellow + : theme.colorScheme.outline, + ), + ); + }).toList(), + ), + SizedBox(height: 3.h), + // Sort Options + Text( + 'Ordenar por', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 2.h), + Column( + children: _sortOptions.map((option) { + final isSelected = _filters['sortBy'] == option; + return RadioListTile( + title: Text( + option, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: + isSelected ? FontWeight.w600 : FontWeight.w400, + ), + ), + value: option, + groupValue: _filters['sortBy'], + onChanged: (value) { + HapticFeedback.lightImpact(); + setState(() { + _filters['sortBy'] = value; + }); + }, + activeColor: AppTheme.primaryBlack, + contentPadding: EdgeInsets.zero, + ); + }).toList(), + ), + SizedBox(height: 2.h), + // Additional Options + Text( + 'Mostrar', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 1.h), + SwitchListTile( + title: Text( + 'Cupones usados', + style: theme.textTheme.bodyMedium, + ), + value: _filters['showUsed'] ?? false, + onChanged: (value) { + HapticFeedback.lightImpact(); + setState(() { + _filters['showUsed'] = value; + }); + }, + activeColor: AppTheme.accentYellow, + activeTrackColor: AppTheme.primaryBlack, + contentPadding: EdgeInsets.zero, + ), + SwitchListTile( + title: Text( + 'Cupones vencidos', + style: theme.textTheme.bodyMedium, + ), + value: _filters['showExpired'] ?? false, + onChanged: (value) { + HapticFeedback.lightImpact(); + setState(() { + _filters['showExpired'] = value; + }); + }, + activeColor: AppTheme.accentYellow, + activeTrackColor: AppTheme.primaryBlack, + contentPadding: EdgeInsets.zero, + ), + ], + ), + ), + ), + // Apply Button + Padding( + padding: EdgeInsets.all(4.w), + child: SizedBox( + width: double.infinity, + height: 6.h, + child: ElevatedButton( + onPressed: _applyFilters, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.accentYellow, + foregroundColor: AppTheme.primaryBlack, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + 'Aplicar Filtros', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + SizedBox(height: MediaQuery.of(context).padding.bottom), + ], + ), + ); + } +} diff --git a/old/lib/presentation/coupons_screen/widgets/sort_dropdown.dart b/old/lib/presentation/coupons_screen/widgets/sort_dropdown.dart new file mode 100644 index 0000000..001aae0 --- /dev/null +++ b/old/lib/presentation/coupons_screen/widgets/sort_dropdown.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../core/app_export.dart'; +import '../../../services/coupon_service.dart'; +import '../../../widgets/custom_icon_widget.dart'; + +class SortDropdown extends StatelessWidget { + final String selectedSort; + final ValueChanged onSortChanged; + + const SortDropdown({ + super.key, + required this.selectedSort, + required this.onSortChanged, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final sortOptions = CouponService.getSortOptions(); + + return Container( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Ordenar por:', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 3.w), + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.outline), + borderRadius: BorderRadius.circular(8), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: selectedSort, + isDense: true, + icon: CustomIconWidget( + iconName: 'keyboard_arrow_down', + color: theme.colorScheme.onSurface, + size: 20, + ), + items: sortOptions.map((String option) { + return DropdownMenuItem( + value: option, + child: Text( + option, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + ); + }).toList(), + onChanged: (String? newValue) { + if (newValue != null) { + HapticFeedback.lightImpact(); + onSortChanged(newValue); + } + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/old/lib/presentation/map_screen/map_screen.dart b/old/lib/presentation/map_screen/map_screen.dart new file mode 100644 index 0000000..a3ddc5a --- /dev/null +++ b/old/lib/presentation/map_screen/map_screen.dart @@ -0,0 +1,760 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +import '../../models/bus_stop_model.dart'; +import '../../models/route_model.dart'; +import '../../services/transportation_service.dart'; +import '../../services/app_state_service.dart'; +import '../../services/supabase_service.dart'; +import '../../widgets/custom_bottom_bar.dart'; +import '../../widgets/route_selection_bottom_sheet.dart'; +import '../../widgets/debug_banner_widget.dart'; +import './widgets/bus_arrival_bottom_sheet.dart'; + +class MapScreen extends StatefulWidget { + const MapScreen({super.key}); + + @override + State createState() => _MapScreenState(); +} + +class _MapScreenState extends State { + final TransportationService _transportationService = TransportationService(); + final AppStateService _appStateService = AppStateService(); + GoogleMapController? _mapController; + + // State variables + List _routeStops = []; + BusStopModel? _selectedStop; + Set _markers = {}; + bool _isLoading = false; + String? _error; + String? _nextBusMessage; + String? _lastRoutesError; + int _routeCount = 0; + + // New connection check state variables + bool _isConnected = false; + String _connectionStatus = 'Not checked'; + + // Panama coordinates (centered around David/Boquete area) + static const LatLng _initialPosition = LatLng(8.4177, -82.4270); + + @override + void initState() { + super.initState(); + _appStateService.addListener(_onGlobalStateChanged); + _performSupabaseConnectionCheck(); + } + + @override + void dispose() { + _appStateService.removeListener(_onGlobalStateChanged); + _mapController?.dispose(); + super.dispose(); + } + + void _onGlobalStateChanged() { + // React to global route selection changes + if (mounted) { + _loadStopsForSelectedRoute(); + } + } + + /// Perform Supabase connection check as specified in requirements + Future _performSupabaseConnectionCheck() async { + if (!mounted) return; + setState(() { + _isLoading = true; + _error = null; + }); + + // Step 1 & 2: Validate credentials and initialize Supabase client + final connectionResult = await SupabaseService.performConnectionCheck(); + + if (connectionResult['success']) { + // Connection successful - show "Connected ✓" and count + if (mounted) { + setState(() { + _isConnected = true; + _connectionStatus = 'Connected ✓'; + _routeCount = connectionResult['count'] ?? 0; + _lastRoutesError = null; + _isLoading = false; + }); + } + + // Load initial data after successful connection + if (mounted) { + await _loadInitialData(); + } + } else { + // Connection failed - show error + final errorMessage = connectionResult['error'] ?? 'Unknown error'; + if (mounted) { + setState(() { + _isConnected = false; + _connectionStatus = 'Connection Failed'; + _lastRoutesError = errorMessage; + _error = errorMessage; + _isLoading = false; + }); + } + + // Show red toast for credential issues + if (errorMessage.contains('Missing') || + errorMessage.contains('SUPABASE_URL') || + errorMessage.contains('SUPABASE_ANON_KEY')) { + Fluttertoast.showToast( + msg: "Missing SUPABASE_URL or SUPABASE_ANON_KEY", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); + } + } + } + + Future _loadInitialData() async { + setState(() { + _isLoading = true; + _error = null; + _lastRoutesError = null; + }); + + try { + // Check if we have routes loaded globally + if (_appStateService.allRoutes.isEmpty && + !_appStateService.isLoadingRoutes) { + await _appStateService.loadRoutes(); + } + + final routes = _appStateService.allRoutes; + if (mounted) { + setState(() { + _routeCount = routes.length; + _lastRoutesError = null; // Clear any previous errors when successful + _isConnected = true; // Update connection status on successful data load + _connectionStatus = 'Connected ✓'; + }); + } + + // Show toast if no routes found as specified in requirements + if (routes.isEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Fluttertoast.showToast( + msg: "No routes available", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.orange, + textColor: Colors.white, + fontSize: 16.0, + ); + }); + if (mounted) { + setState(() { + _isLoading = false; + }); + } + return; + } + + // Auto-pick the first route if no route is selected (Step 6 from requirements) + if (mounted && _appStateService.selectedRouteId == null && routes.isNotEmpty) { + final firstRoute = routes.first; + await _appStateService.selectRoute(firstRoute.id); + } + + // Load stops for currently selected route + if (mounted) { + await _loadStopsForSelectedRoute(); + } + } catch (e) { + if (mounted) { + setState(() { + _error = 'Error loading routes: ${e.toString()}'; + _lastRoutesError = e.toString(); + _isLoading = false; + _isConnected = false; + _connectionStatus = 'Connection Failed'; + }); + } + + // Show Supabase connection error as per requirements + if (e.toString().contains('connection') || + e.toString().contains('credentials')) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Fluttertoast.showToast( + msg: "Could not connect to Supabase. Please check credentials.", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); + }); + } + } + } + + Future _loadStopsForSelectedRoute() async { + final selectedRouteId = _appStateService.selectedRouteId; + + if (selectedRouteId == null) { + setState(() { + _routeStops = []; + _markers = {}; + _selectedStop = null; + _nextBusMessage = null; + _isLoading = false; + }); + return; + } + + setState(() { + _isLoading = true; + _error = null; + _selectedStop = null; + _nextBusMessage = null; + }); + + try { + // Use the exact query from requirements (Step 7) + final stops = await _transportationService.getRouteStopsOrderedBySeq( + selectedRouteId, + ); + + if (mounted) { + setState(() { + _routeStops = stops; + _isLoading = false; + }); + } + + // Clear and re-render markers + if (mounted) { + await _updateMapMarkers(); + } + + // Move camera to show all stops + if (mounted && stops.isNotEmpty && _mapController != null) { + _fitCameraToStops(stops); + } + } catch (e) { + if (mounted) { + setState(() { + _error = 'Error loading stops: ${e.toString()}'; + _isLoading = false; + _routeStops = []; + _markers = {}; + }); + } + } + } + + Future _updateMapMarkers() async { + Set markers = {}; + + for (int i = 0; i < _routeStops.length; i++) { + final stop = _routeStops[i]; + final isSelected = _selectedStop?.id == stop.id; + + markers.add( + Marker( + markerId: MarkerId(stop.id), + position: LatLng(stop.lat, stop.lng), + onTap: () => _onStopTapped(stop), + icon: await _createStopMarkerIcon( + isSelected: isSelected, + stopNumber: (i + 1).toString(), + ), + infoWindow: InfoWindow(title: stop.name, snippet: 'Parada ${i + 1}'), + ), + ); + } + + if (mounted) { + setState(() { + _markers = markers; + }); + } + } + + Future _createStopMarkerIcon({ + required bool isSelected, + required String stopNumber, + }) async { + if (isSelected) { + return BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen); + } else { + return BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueYellow); + } + } + + Future _onStopTapped(BusStopModel stop) async { + if (!mounted) return; + setState(() { + _selectedStop = stop; + _nextBusMessage = null; + }); + + if (mounted) { + await _updateMapMarkers(); + } + + // Calculate next bus time for this stop + if (mounted) { + final selectedRouteId = _appStateService.selectedRouteId; + if (selectedRouteId != null) { + await _calculateNextBusTime(stop, selectedRouteId); + } + } + } + + Future _calculateNextBusTime(BusStopModel stop, String routeId) async { + try { + final nextBusInfo = await _transportationService.getNextBusTime( + routeId, + stop.id, + ); + + if (nextBusInfo != null) { + if (nextBusInfo['minutes_until_arrival'] != null) { + final minutes = nextBusInfo['minutes_until_arrival'] ?? 0; + final scheduleType = nextBusInfo['schedule_type'] ?? 'weekday'; + + String scheduleDisplay = ''; + switch (scheduleType) { + case 'weekday': + scheduleDisplay = 'Lunes-Viernes'; + break; + case 'saturday': + scheduleDisplay = 'Sábado'; + break; + case 'sunday': + scheduleDisplay = 'Domingo'; + break; + } + + if (mounted) { + setState(() { + _nextBusMessage = 'Próximo bus en: $minutes min ($scheduleDisplay)'; + }); + } + } else if (nextBusInfo['first_tomorrow'] != null) { + if (mounted) { + setState(() { + _nextBusMessage = + 'No hay más buses hoy. Primer bus mañana: ${nextBusInfo['first_tomorrow']}'; + }); + } + } + } else { + if (mounted) { + setState(() { + _nextBusMessage = 'No hay más buses programados'; + }); + } + } + } catch (e) { + if (mounted) { + setState(() { + _nextBusMessage = 'Error calculando próximo bus'; + }); + } + } + } + + void _showBusArrivalInfo(BusStopModel stop, RouteModel route) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => DraggableScrollableSheet( + initialChildSize: 0.6, + maxChildSize: 0.9, + minChildSize: 0.3, + builder: (context, scrollController) => + BusArrivalBottomSheet(busStop: stop, route: route), + ), + ); + } + + void _showRouteSelector() { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (context) => RouteSelectionBottomSheet( + title: 'Seleccionar Ruta', + onRouteChanged: () { + // The global state listener will handle the reload + }, + ), + ); + } + + void _fitCameraToStops(List stops) { + if (stops.isEmpty || _mapController == null) return; + + double minLat = stops.first.lat; + double maxLat = stops.first.lat; + double minLng = stops.first.lng; + double maxLng = stops.first.lng; + + for (final stop in stops) { + minLat = math.min(minLat, stop.lat); + maxLat = math.max(maxLat, stop.lat); + minLng = math.min(minLng, stop.lng); + maxLng = math.max(maxLng, stop.lng); + } + + _mapController!.animateCamera( + CameraUpdate.newLatLngBounds( + LatLngBounds( + southwest: LatLng(minLat - 0.01, minLng - 0.01), + northeast: LatLng(maxLat + 0.01, maxLng + 0.01), + ), + 100.0, + ), + ); + } + + void _onMapCreated(GoogleMapController controller) { + _mapController = controller; + if (_routeStops.isNotEmpty) { + _fitCameraToStops(_routeStops); + } + } + + @override + Widget build(BuildContext context) { + final selectedRoute = _appStateService.getSelectedRoute(); + final selectedRouteName = _appStateService.selectedRouteName; + + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: const Color(0xFFFEE715), + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.menu, color: Color(0xFF101820)), + onPressed: () { + // TODO: Implement menu functionality + }, + ), + title: const Text( + 'SIBU', + style: TextStyle( + color: Color(0xFF101820), + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ), + centerTitle: true, + actions: [ + IconButton( + icon: const Icon(Icons.feedback, color: Color(0xFF101820)), + onPressed: () { + // TODO: Implement feedback functionality + }, + ), + ], + ), + body: RefreshIndicator( + onRefresh: _performSupabaseConnectionCheck, + child: Stack( + children: [ + // Google Map + GoogleMap( + onMapCreated: _onMapCreated, + initialCameraPosition: const CameraPosition( + target: _initialPosition, + zoom: 11.0, + ), + markers: _markers, + myLocationEnabled: true, + myLocationButtonEnabled: false, + zoomControlsEnabled: false, + mapToolbarEnabled: false, + ), + + // Debug banner with updated connection status + DebugBannerWidget( + lastError: _lastRoutesError, + routeCount: _routeCount, + isConnected: _isConnected, + connectionStatus: _connectionStatus, + ), + + // Route selector card (always visible when route is selected) + if (selectedRoute != null && selectedRouteName != null) + Positioned( + top: 80, + left: 16, + right: 16, + child: GestureDetector( + onTap: _showRouteSelector, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(26), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Icon( + Icons.route, + color: const Color(0xFF101820), + size: 24, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Route: $selectedRouteName', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFF101820), + ), + ), + Text( + '${_routeStops.length} stops', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + ), + ), + Icon( + Icons.keyboard_arrow_down, + color: Colors.grey[600], + size: 20, + ), + ], + ), + ), + ), + ), + + // Next bus info (when stop is selected) + if (_selectedStop != null && selectedRoute != null) + Positioned( + bottom: 100, + left: 16, + right: 16, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFFEE715), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(26), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _selectedStop!.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF101820), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + _nextBusMessage ?? 'Calculando próximo bus...', + style: TextStyle( + fontSize: 14, + color: const Color(0xFF101820).withAlpha(204), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => _showBusArrivalInfo( + _selectedStop!, + selectedRoute, + ), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF101820), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: const Text( + 'CONSULTAR SIGUIENTE BUS', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ), + + // Loading indicator + if (_isLoading) + Container( + color: Colors.black.withAlpha(77), + child: const Center( + child: CircularProgressIndicator( + valueColor: + AlwaysStoppedAnimation(Color(0xFFFEE715)), + ), + ), + ), + + // Error message (Step 4: red error card) + if (_error != null) + Positioned( + top: 150, + left: 16, + right: 16, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.red[100], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.red[300]!), + ), + child: Row( + children: [ + Icon(Icons.error_outline, + color: Colors.red[600], size: 24), + const SizedBox(width: 12), + Expanded( + child: Text( + _error!, + style: + TextStyle(color: Colors.red[600], fontSize: 14), + ), + ), + IconButton( + onPressed: () { + setState(() => _error = null); + _performSupabaseConnectionCheck(); + }, + icon: Icon( + Icons.refresh, + color: Colors.red[600], + size: 20, + ), + ), + ], + ), + ), + ), + + // No routes found message (Step 4) + if (_routeCount == 0 && !_isLoading && _error == null) + Positioned( + top: 150, + left: 16, + right: 16, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.orange[100], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.orange[300]!), + ), + child: Row( + children: [ + Icon( + Icons.warning_outlined, + color: Colors.orange[600], + size: 24, + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'No routes available', + style: TextStyle(fontSize: 14), + ), + ), + IconButton( + onPressed: _performSupabaseConnectionCheck, + icon: Icon( + Icons.refresh, + color: Colors.orange[600], + size: 20, + ), + ), + ], + ), + ), + ), + + // Route selector floating button (when no route selected) + if (selectedRoute == null && + !_appStateService.isLoadingRoutes && + _routeCount > 0) + Positioned( + top: 80, + right: 16, + child: FloatingActionButton( + mini: true, + backgroundColor: const Color(0xFFFEE715), + onPressed: _showRouteSelector, + child: const Icon(Icons.route, color: Color(0xFF101820)), + ), + ), + + // Small refresh icon with updated refresh method + if (selectedRoute != null) + Positioned( + top: 80, + right: 16, + child: FloatingActionButton( + mini: true, + backgroundColor: const Color(0xFFFEE715), + onPressed: + _performSupabaseConnectionCheck, // Updated to use connection check + child: const Icon(Icons.refresh, color: Color(0xFF101820)), + ), + ), + ], + ), + ), + bottomNavigationBar: CustomBottomBar( + currentIndex: 0, + onTap: (int index) { + // Handle navigation tap + }, + ), + ); + } +} \ No newline at end of file diff --git a/old/lib/presentation/map_screen/widgets/bus_arrival_bottom_sheet.dart b/old/lib/presentation/map_screen/widgets/bus_arrival_bottom_sheet.dart new file mode 100644 index 0000000..c5025f0 --- /dev/null +++ b/old/lib/presentation/map_screen/widgets/bus_arrival_bottom_sheet.dart @@ -0,0 +1,461 @@ +import 'package:flutter/material.dart'; + +import '../../../models/bus_stop_model.dart'; +import '../../../models/route_model.dart'; +import '../../../services/transportation_service.dart'; + +class BusArrivalBottomSheet extends StatefulWidget { + final BusStopModel busStop; + final RouteModel route; + + const BusArrivalBottomSheet({ + super.key, + required this.busStop, + required this.route, + }); + + @override + State createState() => _BusArrivalBottomSheetState(); +} + +class _BusArrivalBottomSheetState extends State { + final TransportationService _transportationService = TransportationService(); + Map? _arrivalInfo; + List> _schedules = []; + bool _isLoading = true; + String? _error; + + @override + void initState() { + super.initState(); + _loadArrivalInfo(); + } + + Future _loadArrivalInfo() async { + try { + setState(() { + _isLoading = true; + _error = null; + }); + + // Load next bus arrival info using the new method with schedule_type + final arrivalInfo = await _transportationService.getNextBusTime( + widget.route.id, + widget.busStop.id, + ); + + // Load all schedules for this route from timetable (current day's schedule) + final schedules = await _transportationService.getRouteTimetables( + widget.route.id, + ); + + setState(() { + _arrivalInfo = arrivalInfo; + _schedules = schedules; + _isLoading = false; + }); + } catch (e) { + setState(() { + _error = 'Error cargando información: ${e.toString()}'; + _isLoading = false; + }); + } + } + + String _formatTime(String timeStr) { + try { + final parts = timeStr.split(':'); + final hour = int.parse(parts[0]); + final minute = int.parse(parts[1]); + final period = hour >= 12 ? 'PM' : 'AM'; + final displayHour = hour > 12 ? hour - 12 : (hour == 0 ? 12 : hour); + return '${displayHour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')} $period'; + } catch (e) { + return timeStr; + } + } + + String _formatMinutesUntil(int minutes) { + if (minutes <= 0) return 'Llegando ahora'; + if (minutes == 1) return 'En 1 minuto'; + if (minutes < 60) return 'En $minutes minutos'; + + final hours = minutes ~/ 60; + final remainingMinutes = minutes % 60; + if (remainingMinutes == 0) { + return hours == 1 ? 'En 1 hora' : 'En $hours horas'; + } + return 'En ${hours}h ${remainingMinutes}min'; + } + + String _getScheduleTypeDisplay(String scheduleType) { + switch (scheduleType) { + case 'weekday': + return 'Lunes-Viernes'; + case 'saturday': + return 'Sábado'; + case 'sunday': + return 'Domingo'; + default: + return scheduleType; + } + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Handle bar + Container( + margin: const EdgeInsets.only(top: 12), + height: 4, + width: 40, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + + // Header + Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: const Color(0xFFFEE715), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.directions_bus, + color: Color(0xFF101820), + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.busStop.displayName, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF101820), + ), + ), + Text( + widget.route.displayName, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + ), + ), + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close), + color: const Color(0xFF101820), + ), + ], + ), + ], + ), + ), + + // Content + Flexible(child: _buildContent()), + ], + ), + ); + } + + Widget _buildContent() { + if (_isLoading) { + return const Padding( + padding: EdgeInsets.all(40), + child: Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Color(0xFFFEE715)), + ), + ), + ); + } + + if (_error != null) { + return Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.error_outline, size: 48, color: Colors.red[400]), + const SizedBox(height: 16), + Text( + _error!, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16, color: Colors.red[600]), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: _loadArrivalInfo, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFEE715), + foregroundColor: const Color(0xFF101820), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text('Reintentar'), + ), + ], + ), + ); + } + + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Next bus info + if (_arrivalInfo != null) ...[ + Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFFFEE715).withAlpha(26), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFFEE715), width: 1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Próximo Bus', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: const Color(0xFF101820), + ), + ), + const SizedBox(height: 8), + Text( + _formatMinutesUntil( + _arrivalInfo!['minutes_until_arrival'] ?? 0, + ), + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Color(0xFF101820), + ), + ), + if (_arrivalInfo!['next_departure'] != null) ...[ + const SizedBox(height: 4), + Text( + 'Salida: ${_formatTime(_arrivalInfo!['next_departure'])}', + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + if (_arrivalInfo!['schedule_type'] != null) ...[ + const SizedBox(height: 2), + Text( + 'Horario: ${_getScheduleTypeDisplay(_arrivalInfo!['schedule_type'])}', + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + ], + ], + ], + ), + ), + const SizedBox(height: 24), + ], + + // Next bus button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _loadArrivalInfo, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF101820), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: const Text( + 'Next bus', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + ), + + const SizedBox(height: 24), + + // Bus stop info + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Información de la Parada', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: const Color(0xFF101820), + ), + ), + const SizedBox(height: 12), + if (widget.busStop.fullAddress.isNotEmpty) ...[ + Row( + children: [ + Icon( + Icons.location_on, + size: 16, + color: Colors.grey[600], + ), + const SizedBox(width: 8), + Expanded( + child: Text( + widget.busStop.fullAddress, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ), + ], + ), + const SizedBox(height: 8), + ], + Row( + children: [ + Icon(Icons.category, size: 16, color: Colors.grey[600]), + const SizedBox(width: 8), + Text( + widget.busStop.stopTypeDisplay, + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + ], + ), + if (widget.busStop.amenities.isNotEmpty) ...[ + const SizedBox(height: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.star, size: 16, color: Colors.grey[600]), + const SizedBox(width: 8), + Expanded( + child: Text( + widget.busStop.amenitiesText, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ), + ], + ), + ], + ], + ), + ), + + // Schedule list + if (_schedules.isNotEmpty) ...[ + const SizedBox(height: 24), + Text( + 'Horarios de Salida', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: const Color(0xFF101820), + ), + ), + const SizedBox(height: 12), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey[300]!), + ), + child: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _schedules.length > 6 ? 6 : _schedules.length, + separatorBuilder: + (context, index) => + Divider(height: 1, color: Colors.grey[200]), + itemBuilder: (context, index) { + final schedule = _schedules[index]; + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + child: Row( + children: [ + Icon(Icons.schedule, size: 16, color: Colors.grey[600]), + const SizedBox(width: 12), + Text( + _formatTime(schedule['departure_time'] ?? ''), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFF101820), + ), + ), + const Spacer(), + if (schedule['frequency_minutes'] != null) + Text( + 'Cada ${schedule['frequency_minutes']} min', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ], + ), + ); + }, + ), + ), + if (_schedules.length > 6) ...[ + const SizedBox(height: 8), + Center( + child: Text( + 'Y ${_schedules.length - 6} horarios más...', + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + ), + ], + ], + + const SizedBox(height: 24), + ], + ), + ); + } +} diff --git a/old/lib/presentation/map_screen/widgets/bus_stop_marker_widget.dart b/old/lib/presentation/map_screen/widgets/bus_stop_marker_widget.dart new file mode 100644 index 0000000..1decdb5 --- /dev/null +++ b/old/lib/presentation/map_screen/widgets/bus_stop_marker_widget.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../core/app_export.dart'; + +class BusStopMarkerWidget extends StatelessWidget { + final Map busStop; + final bool isSelected; + final VoidCallback onTap; + + const BusStopMarkerWidget({ + super.key, + required this.busStop, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: isSelected ? 48.w : 40.w, + height: isSelected ? 48.w : 40.w, + decoration: BoxDecoration( + color: isSelected + ? AppTheme.accentYellow + : AppTheme.accentYellow.withValues(alpha: 0.9), + shape: BoxShape.circle, + border: Border.all( + color: AppTheme.primaryBlack, + width: isSelected ? 3 : 2, + ), + boxShadow: [ + BoxShadow( + color: AppTheme.primaryBlack.withValues(alpha: 0.3), + blurRadius: isSelected ? 8 : 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Center( + child: CustomIconWidget( + iconName: 'directions_bus', + color: AppTheme.primaryBlack, + size: isSelected ? 24 : 20, + ), + ), + ), + ); + } +} diff --git a/old/lib/presentation/map_screen/widgets/loading_overlay_widget.dart b/old/lib/presentation/map_screen/widgets/loading_overlay_widget.dart new file mode 100644 index 0000000..f797273 --- /dev/null +++ b/old/lib/presentation/map_screen/widgets/loading_overlay_widget.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../core/app_export.dart'; +import '../../../theme/app_theme.dart'; + +class LoadingOverlayWidget extends StatefulWidget { + final bool isVisible; + final String message; + + const LoadingOverlayWidget({ + super.key, + required this.isVisible, + this.message = 'Cargando...', + }); + + @override + State createState() => _LoadingOverlayWidgetState(); +} + +class _LoadingOverlayWidgetState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + )); + } + + @override + void didUpdateWidget(LoadingOverlayWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isVisible != oldWidget.isVisible) { + if (widget.isVisible) { + _animationController.forward(); + } else { + _animationController.reverse(); + } + } + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (!widget.isVisible && _animationController.isDismissed) { + return const SizedBox.shrink(); + } + + return AnimatedBuilder( + animation: _fadeAnimation, + builder: (context, child) { + return Opacity( + opacity: _fadeAnimation.value, + child: Container( + width: double.infinity, + height: double.infinity, + color: AppTheme.primaryBlack.withValues(alpha: 0.3), + child: Center( + child: Container( + padding: EdgeInsets.all(6.w), + decoration: BoxDecoration( + color: AppTheme.lightTheme.colorScheme.surface, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: AppTheme.primaryBlack.withValues(alpha: 0.2), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 12.w, + height: 12.w, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + AppTheme.accentYellow, + ), + strokeWidth: 3, + ), + ), + SizedBox(height: 3.h), + Text( + widget.message, + style: AppTheme.lightTheme.textTheme.bodyMedium?.copyWith( + color: AppTheme.primaryBlack, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/old/lib/presentation/map_screen/widgets/map_controls_widget.dart b/old/lib/presentation/map_screen/widgets/map_controls_widget.dart new file mode 100644 index 0000000..6675798 --- /dev/null +++ b/old/lib/presentation/map_screen/widgets/map_controls_widget.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../core/app_export.dart'; + +class MapControlsWidget extends StatelessWidget { + final VoidCallback onLocationPressed; + final VoidCallback onZoomIn; + final VoidCallback onZoomOut; + final bool isLocationEnabled; + + const MapControlsWidget({ + super.key, + required this.onLocationPressed, + required this.onZoomIn, + required this.onZoomOut, + this.isLocationEnabled = true, + }); + + @override + Widget build(BuildContext context) { + return Positioned( + right: 4.w, + bottom: 25.h, + child: Column( + children: [ + // Zoom In Button + _buildControlButton( + icon: 'add', + onPressed: () { + HapticFeedback.lightImpact(); + onZoomIn(); + }, + tooltip: 'Acercar', + ), + + SizedBox(height: 1.h), + + // Zoom Out Button + _buildControlButton( + icon: 'remove', + onPressed: () { + HapticFeedback.lightImpact(); + onZoomOut(); + }, + tooltip: 'Alejar', + ), + + SizedBox(height: 2.h), + + // Location Button + _buildControlButton( + icon: 'my_location', + onPressed: isLocationEnabled + ? () { + HapticFeedback.mediumImpact(); + onLocationPressed(); + } + : null, + tooltip: 'Mi ubicación', + isLocationButton: true, + isEnabled: isLocationEnabled, + ), + ], + ), + ); + } + + Widget _buildControlButton({ + required String icon, + required VoidCallback? onPressed, + required String tooltip, + bool isLocationButton = false, + bool isEnabled = true, + }) { + return Container( + width: 12.w, + height: 12.w, + decoration: BoxDecoration( + color: isEnabled + ? AppTheme.lightTheme.colorScheme.surface + : AppTheme.lightTheme.colorScheme.surface.withValues(alpha: 0.7), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: AppTheme.primaryBlack.withValues(alpha: 0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(8), + child: Center( + child: CustomIconWidget( + iconName: icon, + color: isEnabled + ? (isLocationButton + ? AppTheme.accentYellow + : AppTheme.primaryBlack) + : AppTheme.textSecondary, + size: 24, + ), + ), + ), + ), + ); + } +} diff --git a/old/lib/presentation/schedules_screen/schedules_screen.dart b/old/lib/presentation/schedules_screen/schedules_screen.dart new file mode 100644 index 0000000..daf1d7c --- /dev/null +++ b/old/lib/presentation/schedules_screen/schedules_screen.dart @@ -0,0 +1,632 @@ +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; + +import '../../core/app_export.dart'; +import '../../models/route_model.dart'; +import '../../services/app_state_service.dart'; +import '../../services/transportation_service.dart'; +import '../../widgets/custom_app_bar.dart'; +import '../../widgets/custom_bottom_bar.dart'; +import '../../widgets/route_selection_bottom_sheet.dart'; +import './widgets/empty_state_widget.dart'; +import './widgets/notification_badge_widget.dart'; +import './widgets/route_selection_card.dart'; +import './widgets/schedule_card.dart'; +import './widgets/search_bar_widget.dart'; + +class SchedulesScreen extends StatefulWidget { + const SchedulesScreen({super.key}); + + @override + State createState() => _SchedulesScreenState(); +} + +class _SchedulesScreenState extends State + with TickerProviderStateMixin { + int _currentBottomIndex = 1; // Schedules tab + String _searchQuery = ''; + final ScrollController _scrollController = ScrollController(); + final GlobalKey _refreshIndicatorKey = + GlobalKey(); + + // Animation controllers + late AnimationController _fadeAnimationController; + late Animation _fadeAnimation; + + // Service and data + final TransportationService _transportationService = TransportationService(); + final AppStateService _appStateService = AppStateService(); + List _routes = []; + List> _schedules = []; + bool _isLoadingSchedules = false; + String _currentScheduleType = 'weekday'; + + // Notification state + final Map _notificationStates = {}; + int _activeNotifications = 0; + + @override + void initState() { + super.initState(); + _initializeAnimations(); + _appStateService.addListener(_onGlobalStateChanged); + _loadInitialData(); + _determineCurrentScheduleType(); + } + + @override + void dispose() { + _appStateService.removeListener(_onGlobalStateChanged); + _fadeAnimationController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + void _initializeAnimations() { + _fadeAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _fadeAnimationController, + curve: Curves.easeInOut, + )); + } + + void _onGlobalStateChanged() { + // React to global route selection changes + if (mounted) { + _loadSchedulesForSelectedRoute(); + } + } + + void _determineCurrentScheduleType() { + // Use Panama timezone as specified in requirements + final panamaNow = + DateTime.now().toUtc().add(Duration(hours: -5)); // Panama is UTC-5 + final dayOfWeek = panamaNow.weekday; + + // Monday = 1, Sunday = 7 + if (dayOfWeek >= 1 && dayOfWeek <= 5) { + _currentScheduleType = 'weekday'; + } else if (dayOfWeek == 6) { + _currentScheduleType = 'saturday'; + } else { + _currentScheduleType = 'sunday'; + } + } + + Future _loadInitialData() async { + // Check if we have routes loaded globally + if (_appStateService.allRoutes.isEmpty && + !_appStateService.isLoadingRoutes) { + await _appStateService.loadRoutes(); + } + + // Show toast if no routes found + if (_appStateService.allRoutes.isEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _appStateService.showNoRoutesToast(context); + }); + } + + // Update local routes reference + setState(() { + _routes = _appStateService.allRoutes; + }); + + // Load schedules for currently selected route + await _loadSchedulesForSelectedRoute(); + } + + Future _loadSchedulesForSelectedRoute() async { + final selectedRouteId = _appStateService.selectedRouteId; + + if (selectedRouteId == null) { + setState(() { + _schedules = []; + _isLoadingSchedules = false; + }); + return; + } + + setState(() { + _isLoadingSchedules = true; + }); + + try { + final schedules = + await _transportationService.getRouteTimetablesByScheduleType( + selectedRouteId, + _currentScheduleType, + ); + + setState(() { + _schedules = schedules; + _isLoadingSchedules = false; + }); + + _fadeAnimationController.forward(); + } catch (e) { + setState(() { + _isLoadingSchedules = false; + _schedules = []; + }); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error loading schedules: ${e.toString()}'), + backgroundColor: Colors.red, + duration: const Duration(seconds: 3), + ), + ); + } + } + } + + List get _filteredRoutes { + if (_searchQuery.isEmpty) return _routes; + return _routes.where((route) { + final name = route.name.toLowerCase(); + final query = _searchQuery.toLowerCase(); + return name.contains(query); + }).toList(); + } + + List> get _filteredSchedules { + if (_searchQuery.isEmpty) return _schedules; + + return _schedules.where((schedule) { + final departureTime = + (schedule['departure_time'] as String).toLowerCase(); + final query = _searchQuery.toLowerCase(); + return departureTime.contains(query); + }).toList(); + } + + void _onBottomNavTap(int index) { + setState(() { + _currentBottomIndex = index; + }); + } + + Future _onRouteSelected(String routeId) async { + await _appStateService.selectRoute(routeId); + setState(() { + _searchQuery = ''; + }); + } + + void _onSearchChanged(String query) { + setState(() { + _searchQuery = query; + }); + } + + void _onSearchClear() { + setState(() { + _searchQuery = ''; + }); + } + + void _toggleNotification(String scheduleId) { + setState(() { + final currentState = _notificationStates[scheduleId] ?? false; + _notificationStates[scheduleId] = !currentState; + + if (!currentState) { + _activeNotifications++; + } else { + _activeNotifications--; + } + }); + } + + Future _onRefresh() async { + await _appStateService.refreshRoutes(); + setState(() { + _routes = _appStateService.allRoutes; + }); + await _loadSchedulesForSelectedRoute(); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Horarios actualizados'), + backgroundColor: AppTheme.lightTheme.colorScheme.tertiary, + duration: const Duration(seconds: 2), + ), + ); + } + } + + void _showRouteSelector() { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (context) => RouteSelectionBottomSheet( + title: 'Seleccionar Ruta', + onRouteChanged: () { + // The global state listener will handle the reload + }, + ), + ); + } + + void _showScheduleContextMenu(String scheduleId, String departureTime) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (context) => Container( + decoration: BoxDecoration( + color: AppTheme.lightTheme.colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 12.w, + height: 0.5.h, + margin: EdgeInsets.symmetric(vertical: 2.h), + decoration: BoxDecoration( + color: AppTheme.lightTheme.colorScheme.outline, + borderRadius: BorderRadius.circular(2), + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Text( + 'Opciones para $departureTime', + style: AppTheme.lightTheme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + SizedBox(height: 2.h), + _buildContextMenuItem( + icon: 'alarm_add', + title: 'Configurar Recordatorio', + onTap: () { + Navigator.pop(context); + _showReminderDialog(scheduleId, departureTime); + }, + ), + _buildContextMenuItem( + icon: 'share', + title: 'Compartir Horario', + onTap: () { + Navigator.pop(context); + _shareSchedule(departureTime); + }, + ), + _buildContextMenuItem( + icon: 'report_problem', + title: 'Reportar Problema', + onTap: () { + Navigator.pop(context); + _reportIssue(scheduleId); + }, + ), + SizedBox(height: 2.h), + ], + ), + ), + ), + ); + } + + Widget _buildContextMenuItem({ + required String icon, + required String title, + required VoidCallback onTap, + }) { + return ListTile( + leading: CustomIconWidget( + iconName: icon, + color: AppTheme.lightTheme.colorScheme.onSurface, + size: 24, + ), + title: Text( + title, + style: AppTheme.lightTheme.textTheme.bodyLarge, + ), + onTap: onTap, + ); + } + + void _showReminderDialog(String scheduleId, String departureTime) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Configurar Recordatorio'), + content: Text( + '¿Deseas recibir una notificación antes de la salida de las $departureTime?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancelar'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + _toggleNotification(scheduleId); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Recordatorio configurado'), + duration: Duration(seconds: 2), + ), + ); + }, + child: const Text('Confirmar'), + ), + ], + ), + ); + } + + void _shareSchedule(String departureTime) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Compartiendo horario de $departureTime'), + duration: const Duration(seconds: 2), + ), + ); + } + + void _reportIssue(String scheduleId) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Reporte enviado. Gracias por tu feedback.'), + duration: Duration(seconds: 2), + ), + ); + } + + String _getScheduleTypeDisplayName() { + switch (_currentScheduleType) { + case 'weekday': + return 'Lunes a Viernes'; + case 'saturday': + return 'Sábado'; + case 'sunday': + return 'Domingo'; + default: + return 'Horario'; + } + } + + Widget _buildRouteSelectionView() { + return Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CustomIconWidget( + iconName: 'schedule', + color: AppTheme.lightTheme.colorScheme.secondary, + size: 20, + ), + SizedBox(width: 2.w), + Text( + 'Horarios para ${_getScheduleTypeDisplayName()}', + style: AppTheme.lightTheme.textTheme.bodyMedium?.copyWith( + color: AppTheme.lightTheme.colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + SearchBarWidget( + hintText: 'Buscar rutas...', + onChanged: _onSearchChanged, + onClear: _onSearchClear, + ), + Expanded( + child: _appStateService.isLoadingRoutes + ? const Center(child: CircularProgressIndicator()) + : _filteredRoutes.isEmpty + ? const EmptyStateWidget( + title: 'No se encontraron rutas', + subtitle: 'Intenta con otros términos de búsqueda', + ) + : ListView.builder( + controller: _scrollController, + itemCount: _filteredRoutes.length, + itemBuilder: (context, index) { + final route = _filteredRoutes[index]; + return RouteSelectionCard( + routeName: route.displayName, + duration: route.direction, + onTap: () => _onRouteSelected(route.id), + ); + }, + ), + ), + ], + ); + } + + Widget _buildScheduleView() { + final selectedRoute = _appStateService.getSelectedRoute(); + final selectedRouteName = _appStateService.selectedRouteName; + + if (selectedRoute == null || selectedRouteName == null) { + return const Center( + child: Text('No route selected'), + ); + } + + return Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h), + decoration: BoxDecoration( + color: AppTheme.lightTheme.colorScheme.surface, + border: Border( + bottom: BorderSide( + color: AppTheme.lightTheme.colorScheme.outline, + width: 1, + ), + ), + ), + child: Row( + children: [ + GestureDetector( + onTap: () { + _appStateService.clearSelectedRoute(); + setState(() { + _searchQuery = ''; + _schedules.clear(); + }); + }, + child: Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: AppTheme.lightTheme.colorScheme.secondary + .withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: CustomIconWidget( + iconName: 'arrow_back', + color: AppTheme.lightTheme.colorScheme.primary, + size: 20, + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: GestureDetector( + onTap: _showRouteSelector, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + selectedRouteName, + style: + AppTheme.lightTheme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + overflow: TextOverflow.ellipsis, + ), + Text( + '${_getScheduleTypeDisplayName()} • ${selectedRoute.direction}', + style: + AppTheme.lightTheme.textTheme.bodySmall?.copyWith( + color: + AppTheme.lightTheme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ], + ), + ), + SearchBarWidget( + hintText: 'Buscar horarios...', + onChanged: _onSearchChanged, + onClear: _onSearchClear, + ), + Expanded( + child: RefreshIndicator( + key: _refreshIndicatorKey, + onRefresh: _onRefresh, + color: AppTheme.lightTheme.colorScheme.secondary, + child: _isLoadingSchedules + ? const Center(child: CircularProgressIndicator()) + : _filteredSchedules.isEmpty + ? const EmptyStateWidget( + title: 'No hay horarios disponibles', + subtitle: 'Intenta actualizar o selecciona otra ruta', + ) + : FadeTransition( + opacity: _fadeAnimation, + child: ListView.builder( + controller: _scrollController, + itemCount: _filteredSchedules.length, + itemBuilder: (context, index) { + final schedule = _filteredSchedules[index]; + final scheduleId = schedule['id'].toString(); + final departureTime = + schedule['departure_time'] as String; + + final timeParts = departureTime.split(':'); + final hour = timeParts[0]; + final minute = timeParts[1]; + final formattedTime = '$hour:$minute'; + + return ScheduleCard( + departureTime: formattedTime, + duration: selectedRoute.direction, + arrivalTime: '', + isNotificationEnabled: + _notificationStates[scheduleId] ?? false, + onNotificationToggle: () => + _toggleNotification(scheduleId), + onLongPress: () => _showScheduleContextMenu( + scheduleId, + formattedTime, + ), + ); + }, + ), + ), + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + final hasSelectedRoute = _appStateService.hasSelectedRoute; + + return Scaffold( + backgroundColor: AppTheme.lightTheme.scaffoldBackgroundColor, + appBar: CustomAppBar( + title: 'Horarios', + actions: [ + NotificationBadgeWidget( + count: _activeNotifications, + child: IconButton( + icon: CustomIconWidget( + iconName: 'notifications', + color: AppTheme.lightTheme.colorScheme.onSurface, + size: 24, + ), + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Tienes $_activeNotifications notificaciones activas'), + duration: const Duration(seconds: 2), + ), + ); + }, + ), + ), + ], + ), + body: SafeArea( + child: !hasSelectedRoute + ? _buildRouteSelectionView() + : _buildScheduleView(), + ), + bottomNavigationBar: CustomBottomBar( + currentIndex: _currentBottomIndex, + onTap: _onBottomNavTap, + ), + ); + } +} diff --git a/old/lib/presentation/schedules_screen/widgets/empty_state_widget.dart b/old/lib/presentation/schedules_screen/widgets/empty_state_widget.dart new file mode 100644 index 0000000..6f309f3 --- /dev/null +++ b/old/lib/presentation/schedules_screen/widgets/empty_state_widget.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../core/app_export.dart'; + +class EmptyStateWidget extends StatelessWidget { + final String title; + final String subtitle; + final String? actionText; + final VoidCallback? onActionPressed; + + const EmptyStateWidget({ + super.key, + required this.title, + required this.subtitle, + this.actionText, + this.onActionPressed, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(8.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 30.w, + height: 30.w, + decoration: BoxDecoration( + color: AppTheme.lightTheme.colorScheme.secondary + .withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(15.w), + ), + child: Center( + child: CustomIconWidget( + iconName: 'schedule', + color: AppTheme.lightTheme.colorScheme.secondary, + size: 15.w, + ), + ), + ), + SizedBox(height: 4.h), + Text( + title, + style: AppTheme.lightTheme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + color: AppTheme.lightTheme.colorScheme.onSurface, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 2.h), + Text( + subtitle, + style: AppTheme.lightTheme.textTheme.bodyMedium?.copyWith( + color: AppTheme.lightTheme.colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + if (actionText != null && onActionPressed != null) ...[ + SizedBox(height: 4.h), + ElevatedButton( + onPressed: onActionPressed, + child: Text(actionText!), + ), + ], + ], + ), + ); + } +} diff --git a/old/lib/presentation/schedules_screen/widgets/notification_badge_widget.dart b/old/lib/presentation/schedules_screen/widgets/notification_badge_widget.dart new file mode 100644 index 0000000..ed01f6a --- /dev/null +++ b/old/lib/presentation/schedules_screen/widgets/notification_badge_widget.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../core/app_export.dart'; +import '../../../theme/app_theme.dart'; + +class NotificationBadgeWidget extends StatelessWidget { + final int count; + final Widget child; + + const NotificationBadgeWidget({ + super.key, + required this.count, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Stack( + clipBehavior: Clip.none, + children: [ + child, + if (count > 0) + Positioned( + right: -1.w, + top: -0.5.h, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: count > 9 ? 1.5.w : 1.w, + vertical: 0.5.h, + ), + decoration: BoxDecoration( + color: AppTheme.lightTheme.colorScheme.error, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: AppTheme.lightTheme.colorScheme.surface, + width: 2, + ), + ), + constraints: BoxConstraints( + minWidth: 5.w, + minHeight: 2.5.h, + ), + child: Text( + count > 99 ? '99+' : count.toString(), + style: AppTheme.lightTheme.textTheme.labelSmall?.copyWith( + color: AppTheme.lightTheme.colorScheme.onError, + fontWeight: FontWeight.w600, + fontSize: 10.sp, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ); + } +} diff --git a/old/lib/presentation/schedules_screen/widgets/route_selection_card.dart b/old/lib/presentation/schedules_screen/widgets/route_selection_card.dart new file mode 100644 index 0000000..acf99cb --- /dev/null +++ b/old/lib/presentation/schedules_screen/widgets/route_selection_card.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../core/app_export.dart'; + +class RouteSelectionCard extends StatelessWidget { + final String routeName; + final String duration; + final VoidCallback onTap; + final bool isSelected; + + const RouteSelectionCard({ + super.key, + required this.routeName, + required this.duration, + required this.onTap, + this.isSelected = false, + }); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + HapticFeedback.lightImpact(); + onTap(); + }, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h), + decoration: BoxDecoration( + color: isSelected + ? AppTheme.lightTheme.colorScheme.secondary + .withValues(alpha: 0.1) + : AppTheme.lightTheme.colorScheme.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected + ? AppTheme.lightTheme.colorScheme.secondary + : AppTheme.lightTheme.colorScheme.outline, + width: isSelected ? 2 : 1, + ), + boxShadow: [ + BoxShadow( + color: AppTheme.lightTheme.shadowColor.withValues(alpha: 0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + routeName, + style: + AppTheme.lightTheme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: isSelected + ? AppTheme.lightTheme.colorScheme.primary + : AppTheme.lightTheme.colorScheme.onSurface, + ), + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 0.5.h), + Text( + 'Duración estimada: $duration', + style: + AppTheme.lightTheme.textTheme.bodySmall?.copyWith( + color: + AppTheme.lightTheme.colorScheme.onSurfaceVariant, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + SizedBox(width: 2.w), + CustomIconWidget( + iconName: 'chevron_right', + color: isSelected + ? AppTheme.lightTheme.colorScheme.secondary + : AppTheme.lightTheme.colorScheme.onSurfaceVariant, + size: 24, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/old/lib/presentation/schedules_screen/widgets/schedule_card.dart b/old/lib/presentation/schedules_screen/widgets/schedule_card.dart new file mode 100644 index 0000000..3e4da90 --- /dev/null +++ b/old/lib/presentation/schedules_screen/widgets/schedule_card.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../core/app_export.dart'; + +class ScheduleCard extends StatefulWidget { + final String departureTime; + final String duration; + final String arrivalTime; + final bool isNotificationEnabled; + final VoidCallback onNotificationToggle; + final VoidCallback? onLongPress; + + const ScheduleCard({ + super.key, + required this.departureTime, + required this.duration, + required this.arrivalTime, + required this.isNotificationEnabled, + required this.onNotificationToggle, + this.onLongPress, + }); + + @override + State createState() => _ScheduleCardState(); +} + +class _ScheduleCardState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 150), + vsync: this, + ); + _scaleAnimation = Tween( + begin: 1.0, + end: 0.95, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + )); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + void _handleTapDown(TapDownDetails details) { + _animationController.forward(); + } + + void _handleTapUp(TapUpDetails details) { + _animationController.reverse(); + } + + void _handleTapCancel() { + _animationController.reverse(); + } + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), + child: AnimatedBuilder( + animation: _scaleAnimation, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: GestureDetector( + onTapDown: _handleTapDown, + onTapUp: _handleTapUp, + onTapCancel: _handleTapCancel, + onLongPress: widget.onLongPress != null + ? () { + HapticFeedback.mediumImpact(); + widget.onLongPress!(); + } + : null, + child: Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: AppTheme.lightTheme.colorScheme.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.lightTheme.colorScheme.outline, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: AppTheme.lightTheme.shadowColor + .withValues(alpha: 0.08), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Salida', + style: AppTheme + .lightTheme.textTheme.labelSmall + ?.copyWith( + color: AppTheme.lightTheme.colorScheme + .onSurfaceVariant, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 0.5.h), + Text( + widget.departureTime, + style: AppTheme + .lightTheme.textTheme.titleLarge + ?.copyWith( + fontWeight: FontWeight.w600, + color: AppTheme + .lightTheme.colorScheme.onSurface, + ), + ), + ], + ), + ), + Container( + padding: EdgeInsets.symmetric( + horizontal: 2.w, vertical: 0.5.h), + decoration: BoxDecoration( + color: AppTheme + .lightTheme.colorScheme.secondary + .withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + widget.duration, + style: AppTheme + .lightTheme.textTheme.labelMedium + ?.copyWith( + color: + AppTheme.lightTheme.colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + 'Llegada', + style: AppTheme + .lightTheme.textTheme.labelSmall + ?.copyWith( + color: AppTheme.lightTheme.colorScheme + .onSurfaceVariant, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 0.5.h), + Text( + widget.arrivalTime, + style: AppTheme + .lightTheme.textTheme.titleLarge + ?.copyWith( + fontWeight: FontWeight.w600, + color: AppTheme + .lightTheme.colorScheme.onSurface, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + SizedBox(width: 3.w), + GestureDetector( + onTap: () { + HapticFeedback.lightImpact(); + widget.onNotificationToggle(); + }, + child: Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: widget.isNotificationEnabled + ? AppTheme.lightTheme.colorScheme.secondary + : Colors.transparent, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: widget.isNotificationEnabled + ? AppTheme.lightTheme.colorScheme.secondary + : AppTheme.lightTheme.colorScheme.outline, + width: 1, + ), + ), + child: CustomIconWidget( + iconName: widget.isNotificationEnabled + ? 'notifications' + : 'notifications_none', + color: widget.isNotificationEnabled + ? AppTheme.lightTheme.colorScheme.onSecondary + : AppTheme + .lightTheme.colorScheme.onSurfaceVariant, + size: 20, + ), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/old/lib/presentation/schedules_screen/widgets/search_bar_widget.dart b/old/lib/presentation/schedules_screen/widgets/search_bar_widget.dart new file mode 100644 index 0000000..c2381a0 --- /dev/null +++ b/old/lib/presentation/schedules_screen/widgets/search_bar_widget.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../core/app_export.dart'; + +class SearchBarWidget extends StatefulWidget { + final String hintText; + final ValueChanged onChanged; + final VoidCallback? onClear; + + const SearchBarWidget({ + super.key, + required this.hintText, + required this.onChanged, + this.onClear, + }); + + @override + State createState() => _SearchBarWidgetState(); +} + +class _SearchBarWidgetState extends State { + final TextEditingController _controller = TextEditingController(); + bool _hasText = false; + + @override + void initState() { + super.initState(); + _controller.addListener(_onTextChanged); + } + + @override + void dispose() { + _controller.removeListener(_onTextChanged); + _controller.dispose(); + super.dispose(); + } + + void _onTextChanged() { + final hasText = _controller.text.isNotEmpty; + if (hasText != _hasText) { + setState(() { + _hasText = hasText; + }); + } + widget.onChanged(_controller.text); + } + + void _clearSearch() { + _controller.clear(); + widget.onClear?.call(); + } + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), + child: TextField( + controller: _controller, + style: AppTheme.lightTheme.textTheme.bodyMedium, + decoration: InputDecoration( + hintText: widget.hintText, + prefixIcon: Padding( + padding: EdgeInsets.all(3.w), + child: CustomIconWidget( + iconName: 'search', + color: AppTheme.lightTheme.colorScheme.onSurfaceVariant, + size: 20, + ), + ), + suffixIcon: _hasText + ? IconButton( + icon: CustomIconWidget( + iconName: 'clear', + color: AppTheme.lightTheme.colorScheme.onSurfaceVariant, + size: 20, + ), + onPressed: _clearSearch, + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.lightTheme.colorScheme.outline, + width: 1, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.lightTheme.colorScheme.outline, + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.lightTheme.colorScheme.primary, + width: 2, + ), + ), + filled: true, + fillColor: AppTheme.lightTheme.colorScheme.surface, + contentPadding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h), + ), + ), + ); + } +} diff --git a/old/lib/presentation/splash_screen/splash_screen.dart b/old/lib/presentation/splash_screen/splash_screen.dart new file mode 100644 index 0000000..6e9f990 --- /dev/null +++ b/old/lib/presentation/splash_screen/splash_screen.dart @@ -0,0 +1,357 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:sizer/sizer.dart'; + +import '../../../core/app_export.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({super.key}); + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State + with TickerProviderStateMixin { + late AnimationController _logoAnimationController; + late AnimationController _loadingAnimationController; + late Animation _logoFadeAnimation; + late Animation _logoScaleAnimation; + late Animation _loadingOpacityAnimation; + + bool _showLoading = false; + bool _initializationComplete = false; + String _statusMessage = 'Iniciando SIBU...'; + + @override + void initState() { + super.initState(); + _setupAnimations(); + _startInitialization(); + } + + void _setupAnimations() { + // Logo animation controller + _logoAnimationController = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + ); + + // Loading animation controller + _loadingAnimationController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + + // Logo fade in animation + _logoFadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _logoAnimationController, + curve: const Interval(0.0, 0.6, curve: Curves.easeOut), + )); + + // Logo scale animation + _logoScaleAnimation = Tween( + begin: 0.8, + end: 1.0, + ).animate(CurvedAnimation( + parent: _logoAnimationController, + curve: const Interval(0.0, 0.8, curve: Curves.elasticOut), + )); + + // Loading indicator opacity animation + _loadingOpacityAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _loadingAnimationController, + curve: Curves.easeIn, + )); + + // Start logo animation + _logoAnimationController.forward(); + } + + Future _startInitialization() async { + try { + // Show loading indicator after logo animation + await Future.delayed(const Duration(milliseconds: 1000)); + setState(() { + _showLoading = true; + }); + _loadingAnimationController.forward(); + + // Simulate initialization tasks + await _performInitializationTasks(); + + // Mark initialization as complete + setState(() { + _initializationComplete = true; + _statusMessage = 'Listo para usar'; + }); + + // Navigate to main screen after brief delay + await Future.delayed(const Duration(milliseconds: 500)); + _navigateToMainScreen(); + } catch (e) { + _handleInitializationError(e); + } + } + + Future _performInitializationTasks() async { + // Task 1: Check GPS permissions + setState(() { + _statusMessage = 'Verificando permisos GPS...'; + }); + await Future.delayed(const Duration(milliseconds: 600)); + + // Task 2: Load cached route data + setState(() { + _statusMessage = 'Cargando datos de rutas...'; + }); + await Future.delayed(const Duration(milliseconds: 700)); + + // Task 3: Fetch latest bus schedules + setState(() { + _statusMessage = 'Actualizando horarios...'; + }); + await Future.delayed(const Duration(milliseconds: 800)); + + // Task 4: Prepare map tiles + setState(() { + _statusMessage = 'Preparando mapas...'; + }); + await Future.delayed(const Duration(milliseconds: 500)); + } + + void _navigateToMainScreen() { + // Navigate to map screen (main tab) + Navigator.pushReplacementNamed(context, '/map-screen'); + } + + void _handleInitializationError(dynamic error) { + setState(() { + _statusMessage = 'Error de conexión'; + }); + + // Show continue offline option after 5 seconds + Future.delayed(const Duration(seconds: 5), () { + if (mounted && !_initializationComplete) { + _showContinueOfflineDialog(); + } + }); + } + + void _showContinueOfflineDialog() { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: AppTheme.lightTheme.colorScheme.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: Text( + 'Continuar sin conexión', + style: AppTheme.lightTheme.textTheme.titleLarge?.copyWith( + color: AppTheme.lightTheme.colorScheme.onSurface, + ), + ), + content: Text( + 'No se pudo conectar al servidor. ¿Deseas continuar con datos guardados?', + style: AppTheme.lightTheme.textTheme.bodyMedium?.copyWith( + color: AppTheme.lightTheme.colorScheme.onSurface, + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + _startInitialization(); // Retry + }, + child: Text( + 'Reintentar', + style: TextStyle( + color: AppTheme.lightTheme.colorScheme.primary, + ), + ), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + _navigateToMainScreen(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.accentYellow, + foregroundColor: AppTheme.primaryBlack, + ), + child: const Text('Continuar'), + ), + ], + ); + }, + ); + } + + Widget _buildLogo() { + return AnimatedBuilder( + animation: _logoAnimationController, + builder: (context, child) { + return Transform.scale( + scale: _logoScaleAnimation.value, + child: Opacity( + opacity: _logoFadeAnimation.value, + child: Container( + width: 35.w, + height: 35.w, + decoration: BoxDecoration( + color: AppTheme.accentYellow, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: AppTheme.primaryBlack.withValues(alpha: 0.2), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CustomIconWidget( + iconName: 'directions_bus', + color: AppTheme.primaryBlack, + size: 12.w, + ), + SizedBox(height: 1.h), + Text( + 'SIBU', + style: + AppTheme.lightTheme.textTheme.headlineSmall?.copyWith( + color: AppTheme.primaryBlack, + fontWeight: FontWeight.bold, + letterSpacing: 2, + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildLoadingIndicator() { + return _showLoading + ? AnimatedBuilder( + animation: _loadingAnimationController, + builder: (context, child) { + return Opacity( + opacity: _loadingOpacityAnimation.value, + child: Column( + children: [ + SizedBox(height: 8.h), + SizedBox( + width: 8.w, + height: 8.w, + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation( + AppTheme.accentYellow, + ), + backgroundColor: + AppTheme.accentYellow.withValues(alpha: 0.3), + ), + ), + SizedBox(height: 3.h), + Text( + _statusMessage, + style: AppTheme.lightTheme.textTheme.bodyMedium?.copyWith( + color: AppTheme.surfaceWhite.withValues(alpha: 0.8), + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + }, + ) + : const SizedBox.shrink(); + } + + Widget _buildVersionInfo() { + return Positioned( + bottom: 8.h, + left: 0, + right: 0, + child: Column( + children: [ + Text( + 'Transporte Público Boquete', + style: AppTheme.lightTheme.textTheme.bodySmall?.copyWith( + color: AppTheme.surfaceWhite.withValues(alpha: 0.6), + ), + ), + SizedBox(height: 1.h), + Text( + 'Versión 1.0.0', + style: AppTheme.lightTheme.textTheme.labelSmall?.copyWith( + color: AppTheme.surfaceWhite.withValues(alpha: 0.4), + ), + ), + ], + ), + ); + } + + @override + void dispose() { + _logoAnimationController.dispose(); + _loadingAnimationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.light, + statusBarBrightness: Brightness.dark, + systemNavigationBarColor: AppTheme.primaryBlack, + systemNavigationBarIconBrightness: Brightness.light, + ), + child: Scaffold( + backgroundColor: AppTheme.primaryBlack, + body: SafeArea( + child: SizedBox( + width: double.infinity, + height: double.infinity, + child: Stack( + children: [ + // Main content + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildLogo(), + _buildLoadingIndicator(), + ], + ), + ), + // Version info + _buildVersionInfo(), + ], + ), + ), + ), + ), + ); + } +} diff --git a/old/lib/presentation/taxi_screen/taxi_screen.dart b/old/lib/presentation/taxi_screen/taxi_screen.dart new file mode 100644 index 0000000..ac0b165 --- /dev/null +++ b/old/lib/presentation/taxi_screen/taxi_screen.dart @@ -0,0 +1,265 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import '../../models/taxi_model.dart'; +import '../../services/taxi_service.dart'; +import '../../widgets/custom_app_bar.dart'; +import '../../widgets/custom_bottom_bar.dart'; +import './widgets/taxi_card_widget.dart'; +import './widgets/taxi_empty_state_widget.dart'; +import './widgets/taxi_filters_widget.dart'; + +/// Main taxi screen with filtering, search, and favorites functionality +class TaxiScreen extends StatefulWidget { + const TaxiScreen({super.key}); + + @override + State createState() => _TaxiScreenState(); +} + +class _TaxiScreenState extends State { + final TaxiService _taxiService = TaxiService.instance; + Timer? _searchDebouncer; + + // State variables + List _corregimientos = []; + List _shifts = []; + List _taxis = []; + Set _favoriteTaxiIds = {}; + String? _selectedCorregimiento; + String? _selectedShift; + String _searchText = ''; + bool _isLoading = true; + String? _error; + + @override + void initState() { + super.initState(); + _loadInitialData(); + } + + @override + void dispose() { + _searchDebouncer?.cancel(); + super.dispose(); + } + + /// Load corregimientos, shifts and initial taxi data + Future _loadInitialData() async { + try { + setState(() { + _isLoading = true; + _error = null; + }); + + // Load corregimientos, shifts and favorites + final results = await Future.wait([ + _taxiService.getCorregimientos(), + _taxiService.getFavoriteTaxiIds(), + ]); + + final corregimientos = results[0]; + final favoriteIds = results[1]; + + setState(() { + _corregimientos = corregimientos; + _shifts = _taxiService.getShifts(); + _favoriteTaxiIds = favoriteIds.toSet(); + _isLoading = false; + }); + + // Load taxis for all corregimientos initially + await _searchTaxis(); + } catch (e) { + setState(() { + _error = e.toString(); + _isLoading = false; + }); + } + } + + /// Search taxis with current filters + Future _searchTaxis() async { + try { + final taxis = await _taxiService.searchTaxis( + selectedCorregimiento: _selectedCorregimiento, + selectedShift: _selectedShift, + searchText: _searchText.isEmpty ? null : _searchText, + ); + + setState(() { + _taxis = taxis; + _error = null; + }); + } catch (e) { + setState(() { + _error = e.toString(); + }); + } + } + + /// Handle corregimiento selection change + void _onCorregimientoChanged(String? corregimiento) { + setState(() { + _selectedCorregimiento = corregimiento; + }); + _searchTaxis(); + } + + /// Handle shift selection change + void _onShiftChanged(String? shift) { + setState(() { + _selectedShift = shift; + }); + _searchTaxis(); + } + + /// Handle search text change with debouncing + void _onSearchChanged(String text) { + setState(() { + _searchText = text; + }); + + // Cancel previous timer + _searchDebouncer?.cancel(); + + // Start new timer for debounced search + _searchDebouncer = Timer(const Duration(milliseconds: 300), () { + _searchTaxis(); + }); + } + + /// Clear all filters + void _onClearFilters() { + setState(() { + _selectedCorregimiento = null; + _selectedShift = null; + _searchText = ''; + }); + _searchTaxis(); + } + + /// Toggle favorite status for a taxi + Future _onFavoriteToggle(String taxiId) async { + try { + final isFavorite = await _taxiService.toggleFavorite(taxiId); + + setState(() { + if (isFavorite) { + _favoriteTaxiIds.add(taxiId); + } else { + _favoriteTaxiIds.remove(taxiId); + } + }); + + // Show feedback + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + isFavorite + ? 'Taxi agregado a favoritos' + : 'Taxi removido de favoritos', + ), + duration: const Duration(seconds: 2), + backgroundColor: isFavorite ? Colors.green : Colors.orange, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error al actualizar favoritos: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + /// Handle pull to refresh + Future _onRefresh() async { + await _loadInitialData(); + } + + /// Get current empty state widget based on context + Widget _getEmptyStateWidget() { + if (_error != null) { + return TaxiEmptyStateWidget.error( + error: _error!, + onRetry: _loadInitialData, + ); + } + + if (_selectedCorregimiento == null && + _selectedShift == null && + _searchText.isEmpty) { + return TaxiEmptyStateWidget.noFiltersSelected(); + } + + return TaxiEmptyStateWidget.noResultsFound(onClearFilters: _onClearFilters); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Scaffold( + backgroundColor: theme.colorScheme.surface, + appBar: const CustomAppBar( + title: 'Taxi Directory', + backgroundColor: Color(0xFF101820), + foregroundColor: Color(0xFFFEE715), + ), + body: Column( + children: [ + // Filters Section + TaxiFiltersWidget( + corregimientos: _corregimientos, + shifts: _shifts, + selectedCorregimiento: _selectedCorregimiento, + selectedShift: _selectedShift, + searchText: _searchText, + onCorregimientoChanged: _onCorregimientoChanged, + onShiftChanged: _onShiftChanged, + onSearchChanged: _onSearchChanged, + onClearFilters: _onClearFilters, + ), + // Results Section + Expanded( + child: + _isLoading + ? const Center(child: CircularProgressIndicator()) + : _taxis.isEmpty + ? _getEmptyStateWidget() + : RefreshIndicator( + onRefresh: _onRefresh, + child: ListView.builder( + itemCount: _taxis.length, + itemBuilder: (context, index) { + final taxi = _taxis[index]; + return TaxiCardWidget( + taxi: taxi, + isFavorite: _favoriteTaxiIds.contains(taxi.id), + onFavoriteToggle: _onFavoriteToggle, + ); + }, + ), + ), + ), + ], + ), + bottomNavigationBar: CustomBottomBar( + currentIndex: 3, // Taxi tab index + onTap: _onBottomNavTap, + ), + ); + } + + /// Handle bottom navigation tap + void _onBottomNavTap(int index) { + // Navigation is handled by CustomBottomBar + } +} diff --git a/old/lib/presentation/taxi_screen/widgets/taxi_card_widget.dart b/old/lib/presentation/taxi_screen/widgets/taxi_card_widget.dart new file mode 100644 index 0000000..f0e1401 --- /dev/null +++ b/old/lib/presentation/taxi_screen/widgets/taxi_card_widget.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../../models/taxi_model.dart'; + +/// Individual taxi card widget displaying taxi information with call and favorite functionality +class TaxiCardWidget extends StatelessWidget { + final TaxiModel taxi; + final bool isFavorite; + final Function(String) onFavoriteToggle; + + const TaxiCardWidget({ + super.key, + required this.taxi, + required this.isFavorite, + required this.onFavoriteToggle, + }); + + /// Launch phone call + Future _makePhoneCall(String phoneNumber) async { + final Uri launchUri = Uri(scheme: 'tel', path: phoneNumber); + try { + if (await canLaunchUrl(launchUri)) { + await launchUrl(launchUri); + } + } catch (e) { + // Handle error silently or show user feedback + } + } + + /// Capitalize first letter of shift for display + String _formatShift(String shift) { + if (shift.isEmpty) return ''; + return shift[0].toUpperCase() + shift.substring(1); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Card( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Row: Name and Favorite Button + Row( + children: [ + Expanded( + child: Text( + taxi.name, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: const Color(0xFF101820), + ), + ), + ), + IconButton( + onPressed: () => onFavoriteToggle(taxi.id), + icon: Icon( + isFavorite ? Icons.favorite : Icons.favorite_border, + color: + isFavorite + ? const Color(0xFFFEE715) + : theme.iconTheme.color, + ), + tooltip: + isFavorite ? 'Remove from favorites' : 'Add to favorites', + ), + ], + ), + const SizedBox(height: 8), + + // Phone Row with Call Button + Row( + children: [ + Icon(Icons.phone, size: 20, color: theme.iconTheme.color), + const SizedBox(width: 8), + Expanded( + child: Text( + taxi.phone, + style: theme.textTheme.bodyMedium?.copyWith( + color: const Color(0xFF101820), + ), + ), + ), + ElevatedButton.icon( + onPressed: () => _makePhoneCall(taxi.phone), + icon: const Icon(Icons.call, size: 18), + label: const Text('Call'), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFEE715), + foregroundColor: const Color(0xFF101820), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], + ), + const SizedBox(height: 8), + + // Location and Shift Info + Row( + children: [ + // Corregimiento + Expanded( + child: Row( + children: [ + Icon( + Icons.location_on, + size: 16, + color: theme.iconTheme.color?.withAlpha(153), + ), + const SizedBox(width: 4), + Text( + taxi.corregimiento, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.textTheme.bodySmall?.color?.withAlpha( + 153, + ), + ), + ), + ], + ), + ), + // Shift + Row( + children: [ + Icon( + Icons.schedule, + size: 16, + color: theme.iconTheme.color?.withAlpha(153), + ), + const SizedBox(width: 4), + Text( + _formatShift(taxi.shift), + style: theme.textTheme.bodySmall?.copyWith( + color: theme.textTheme.bodySmall?.color?.withAlpha(153), + ), + ), + ], + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/old/lib/presentation/taxi_screen/widgets/taxi_empty_state_widget.dart b/old/lib/presentation/taxi_screen/widgets/taxi_empty_state_widget.dart new file mode 100644 index 0000000..f07a6ec --- /dev/null +++ b/old/lib/presentation/taxi_screen/widgets/taxi_empty_state_widget.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; + +/// Widget for displaying various empty states in the taxi screen +class TaxiEmptyStateWidget extends StatelessWidget { + final String title; + final String message; + final IconData icon; + final String? buttonText; + final VoidCallback? onButtonPressed; + + const TaxiEmptyStateWidget({ + super.key, + required this.title, + required this.message, + required this.icon, + this.buttonText, + this.onButtonPressed, + }); + + /// Empty state when no filters are selected + factory TaxiEmptyStateWidget.noFiltersSelected() { + return const TaxiEmptyStateWidget( + title: 'Select Filters', + message: 'Choose a corregimiento and/or shift to see available taxis.', + icon: Icons.filter_list, + ); + } + + /// Empty state when no results match the current filters + factory TaxiEmptyStateWidget.noResultsFound({ + required VoidCallback onClearFilters, + }) { + return TaxiEmptyStateWidget( + title: 'No taxis available for this selection.', + message: 'Try adjusting your filters or search terms.', + icon: Icons.search_off, + buttonText: 'Clear Filters', + onButtonPressed: onClearFilters, + ); + } + + /// Empty state when there's no data at all + factory TaxiEmptyStateWidget.noData() { + return const TaxiEmptyStateWidget( + title: 'No taxis registered yet.', + message: 'There are no taxi services available at the moment.', + icon: Icons.local_taxi, + ); + } + + /// Empty state for error conditions + factory TaxiEmptyStateWidget.error({ + required String error, + required VoidCallback onRetry, + }) { + return TaxiEmptyStateWidget( + title: 'Error Loading Taxis', + message: error, + icon: Icons.error_outline, + buttonText: 'Retry', + onButtonPressed: onRetry, + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 80, color: theme.iconTheme.color?.withAlpha(128)), + const SizedBox(height: 24), + Text( + title, + style: theme.textTheme.headlineSmall?.copyWith( + color: const Color(0xFF101820), + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + Text( + message, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.textTheme.bodyMedium?.color?.withAlpha(153), + ), + textAlign: TextAlign.center, + ), + if (buttonText != null && onButtonPressed != null) ...[ + const SizedBox(height: 24), + ElevatedButton( + onPressed: onButtonPressed, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFEE715), + foregroundColor: const Color(0xFF101820), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text(buttonText!), + ), + ], + ], + ), + ), + ); + } +} diff --git a/old/lib/presentation/taxi_screen/widgets/taxi_filters_widget.dart b/old/lib/presentation/taxi_screen/widgets/taxi_filters_widget.dart new file mode 100644 index 0000000..0dfc82f --- /dev/null +++ b/old/lib/presentation/taxi_screen/widgets/taxi_filters_widget.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; + +/// Widget for taxi search filters including corregimiento dropdown, shift dropdown and search input +class TaxiFiltersWidget extends StatefulWidget { + final List corregimientos; // Changed from districts + final List shifts; // Added shifts list + final String? selectedCorregimiento; // Changed from selectedDistrict + final String? selectedShift; // Added shift selection + final String searchText; + final Function(String?) + onCorregimientoChanged; // Changed from onDistrictChanged + final Function(String?) onShiftChanged; // Added shift handler + final Function(String) onSearchChanged; + final VoidCallback onClearFilters; + + const TaxiFiltersWidget({ + super.key, + required this.corregimientos, // Changed from districts + required this.shifts, // Added shifts + required this.selectedCorregimiento, // Changed from selectedDistrict + required this.selectedShift, // Added shift selection + required this.searchText, + required this.onCorregimientoChanged, // Changed from onDistrictChanged + required this.onShiftChanged, // Added shift handler + required this.onSearchChanged, + required this.onClearFilters, + }); + + @override + State createState() => _TaxiFiltersWidgetState(); +} + +class _TaxiFiltersWidgetState extends State { + late TextEditingController _searchController; + + @override + void initState() { + super.initState(); + _searchController = TextEditingController(text: widget.searchText); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + boxShadow: [ + BoxShadow( + color: theme.shadowColor.withAlpha(26), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + // Filter Dropdowns Row + Row( + children: [ + // Corregimiento Dropdown + Expanded( + child: DropdownButtonFormField( + value: + widget + .selectedCorregimiento, // Changed from selectedDistrict + hint: const Text( + 'Seleccionar Corregimiento', + ), // Updated hint text + decoration: InputDecoration( + labelText: + 'Filter by Corregimiento', // Updated label as requested + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + ), + items: [ + const DropdownMenuItem( + value: null, + child: Text('Todos los corregimientos'), // Updated text + ), + ...widget.corregimientos.map( + // Changed from districts + (corregimiento) => DropdownMenuItem( + value: corregimiento, + child: Text(corregimiento), + ), + ), + ], + onChanged: + widget + .onCorregimientoChanged, // Changed from onDistrictChanged + ), + ), + const SizedBox(width: 12), + // Shift Dropdown + Expanded( + child: DropdownButtonFormField( + value: widget.selectedShift, + hint: const Text('Seleccionar Turno'), + decoration: InputDecoration( + labelText: 'Filter by Shift', // Label as requested + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + ), + items: [ + const DropdownMenuItem( + value: null, + child: Text('Todos los turnos'), + ), + ...widget.shifts.map( + (shift) => DropdownMenuItem( + value: shift, + child: Text(shift), + ), + ), + ], + onChanged: widget.onShiftChanged, + ), + ), + if (widget.selectedCorregimiento != + null || // Changed from selectedDistrict + widget.selectedShift != null || // Added shift condition + widget.searchText.isNotEmpty) ...[ + const SizedBox(width: 8), + IconButton( + onPressed: widget.onClearFilters, + icon: const Icon(Icons.clear), + tooltip: 'Clear Filters', // Updated tooltip text as requested + style: IconButton.styleFrom( + backgroundColor: theme.colorScheme.errorContainer, + foregroundColor: theme.colorScheme.onErrorContainer, + ), + ), + ], + ], + ), + const SizedBox(height: 12), + // Search Input + TextField( + controller: _searchController, + decoration: InputDecoration( + labelText: 'Buscar taxi o teléfono', + hintText: 'Escribe el nombre o número...', + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + suffixIcon: + widget.searchText.isNotEmpty + ? IconButton( + onPressed: () { + _searchController.clear(); + widget.onSearchChanged(''); + }, + icon: const Icon(Icons.clear), + tooltip: 'Limpiar búsqueda', + ) + : null, + ), + onChanged: widget.onSearchChanged, + textInputAction: TextInputAction.search, + ), + ], + ), + ); + } +} diff --git a/old/lib/routes/app_routes.dart b/old/lib/routes/app_routes.dart new file mode 100644 index 0000000..7059f23 --- /dev/null +++ b/old/lib/routes/app_routes.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import '../presentation/bus_stop_details/bus_stop_details.dart'; +import '../presentation/splash_screen/splash_screen.dart'; +import '../presentation/coupons_screen/coupons_screen.dart'; +import '../presentation/schedules_screen/schedules_screen.dart'; +import '../presentation/map_screen/map_screen.dart'; +import '../presentation/taxi_screen/taxi_screen.dart'; + +class AppRoutes { + // TODO: Add your routes here + static const String initial = '/'; + static const String busStopDetails = '/bus-stop-details'; + static const String splash = '/splash-screen'; + static const String coupons = '/coupons-screen'; + static const String schedules = '/schedules-screen'; + static const String map = '/map-screen'; + static const String taxi = '/taxi-screen'; + + static Map routes = { + initial: (context) => const SplashScreen(), + busStopDetails: (context) => const BusStopDetails(), + splash: (context) => const SplashScreen(), + coupons: (context) => const CouponsScreen(), + schedules: (context) => const SchedulesScreen(), + map: (context) => const MapScreen(), + taxi: (context) => const TaxiScreen(), + // TODO: Add your other routes here + }; +} diff --git a/old/lib/services/api_client.dart b/old/lib/services/api_client.dart new file mode 100644 index 0000000..f64c8e8 --- /dev/null +++ b/old/lib/services/api_client.dart @@ -0,0 +1,170 @@ +import 'dart:convert'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; + +/// API Client for connecting to the FastAPI backend +class ApiClient { + static ApiClient? _instance; + static ApiClient get instance => _instance ??= ApiClient._(); + + late Dio _dio; + String _baseUrl = 'http://localhost:8000'; + + ApiClient._() { + _dio = Dio(BaseOptions( + baseUrl: _baseUrl, + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 10), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + )); + + // Add interceptors for logging in debug mode + if (kDebugMode) { + _dio.interceptors.add(LogInterceptor( + requestBody: true, + responseBody: true, + error: true, + )); + } + + // Error handling interceptor + _dio.interceptors.add(InterceptorsWrapper( + onError: (error, handler) { + if (error.response != null) { + debugPrint('API Error: ${error.response?.statusCode} - ${error.response?.data}'); + } else { + debugPrint('API Error: ${error.message}'); + } + handler.next(error); + }, + )); + } + + /// Initialize with custom base URL + void initialize({String? baseUrl}) { + final url = baseUrl ?? getBaseUrl(); + _baseUrl = url; + _dio.options.baseUrl = _baseUrl; + } + + /// Get base URL from environment or use default + static String getBaseUrl() { + const url = String.fromEnvironment('API_BASE_URL'); + return url.isNotEmpty ? url : 'http://localhost:8000'; + } + + /// Get current base URL + String get baseUrl => _baseUrl; + + /// GET request + Future> get( + String path, { + Map? queryParameters, + Options? options, + }) async { + try { + return await _dio.get( + path, + queryParameters: queryParameters, + options: options, + ); + } catch (e) { + if (e is DioException) { + rethrow; + } + throw DioException( + requestOptions: RequestOptions(path: path), + error: e, + ); + } + } + + /// POST request + Future> post( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + }) async { + try { + return await _dio.post( + path, + data: data, + queryParameters: queryParameters, + options: options, + ); + } catch (e) { + if (e is DioException) { + rethrow; + } + throw DioException( + requestOptions: RequestOptions(path: path), + error: e, + ); + } + } + + /// PUT request + Future> put( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + }) async { + try { + return await _dio.put( + path, + data: data, + queryParameters: queryParameters, + options: options, + ); + } catch (e) { + if (e is DioException) { + rethrow; + } + throw DioException( + requestOptions: RequestOptions(path: path), + error: e, + ); + } + } + + /// DELETE request + Future> delete( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + }) async { + try { + return await _dio.delete( + path, + data: data, + queryParameters: queryParameters, + options: options, + ); + } catch (e) { + if (e is DioException) { + rethrow; + } + throw DioException( + requestOptions: RequestOptions(path: path), + error: e, + ); + } + } + + /// Check if API is available + Future checkConnection() async { + try { + final response = await _dio.get('/health'); + return response.statusCode == 200; + } catch (e) { + return false; + } + } +} + diff --git a/old/lib/services/app_state_service.dart b/old/lib/services/app_state_service.dart new file mode 100644 index 0000000..cfd72cd --- /dev/null +++ b/old/lib/services/app_state_service.dart @@ -0,0 +1,207 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +import '../models/route_model.dart'; +import './transportation_service.dart'; + +/// Global app state service to manage selected route across the entire app +class AppStateService extends ChangeNotifier { + static final AppStateService _instance = AppStateService._internal(); + factory AppStateService() => _instance; + AppStateService._internal(); + + final TransportationService _transportationService = TransportationService(); + + // Global state + String? _selectedRouteId; + String? _selectedRouteName; + List _allRoutes = []; + bool _isLoadingRoutes = false; + String? _error; + + // Getters + String? get selectedRouteId => _selectedRouteId; + String? get selectedRouteName => _selectedRouteName; + List get allRoutes => List.unmodifiable(_allRoutes); + bool get isLoadingRoutes => _isLoadingRoutes; + String? get error => _error; + bool get hasSelectedRoute => + _selectedRouteId != null && _selectedRouteName != null; + + /// Initialize app state - call this on app start + Future initialize() async { + await loadRoutes(); + } + + /// Load all routes from Supabase + Future loadRoutes() async { + if (_isLoadingRoutes) return; + + _isLoadingRoutes = true; + notifyListeners(); + + try { + final result = await _transportationService.getRoutesWithCount(); + final routes = result['data'] as List; + final count = result['count'] as int; + final error = result['error'] as String?; + + if (error != null) { + print('Error loading routes: $error'); + // Handle error but continue with empty list + _allRoutes = []; + } else { + _allRoutes = routes; + + // Auto-select first route if none selected and routes available + if (routes.isNotEmpty && _selectedRouteId == null) { + await selectRoute(routes.first.id); + } + } + + _isLoadingRoutes = false; + notifyListeners(); + + // Show toast if no routes found as specified in requirements + if (_allRoutes.isEmpty) { + _showNoRoutesToast(); + } + } catch (e) { + print('Exception loading routes: $e'); + _allRoutes = []; + _isLoadingRoutes = false; + notifyListeners(); + + // Show error toast for exceptions + Fluttertoast.showToast( + msg: "Error loading routes: ${e.toString()}", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); + } + } + + void _showNoRoutesToast() { + Fluttertoast.showToast( + msg: "No routes found", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.orange, + textColor: Colors.white, + fontSize: 16.0, + ); + } + + /// Select a route and update global state + Future selectRoute(String routeId) async { + if (routeId == _selectedRouteId) return; + + try { + // Find the route in our cached routes + final route = _allRoutes.firstWhere( + (r) => r.id == routeId, + orElse: () => throw Exception('Route not found: $routeId'), + ); + + _selectedRouteId = routeId; + _selectedRouteName = route.displayName; + _error = null; + + debugPrint('✅ Route selected: $routeId - $_selectedRouteName'); + notifyListeners(); + } catch (e) { + _error = 'Error selecting route: $e'; + debugPrint('❌ Error selecting route: $e'); + notifyListeners(); + } + } + + /// Clear selected route + void clearSelectedRoute() { + _selectedRouteId = null; + _selectedRouteName = null; + notifyListeners(); + } + + /// Get selected route model + RouteModel? getSelectedRoute() { + if (_selectedRouteId == null) return null; + try { + return _allRoutes.firstWhere((r) => r.id == _selectedRouteId!); + } catch (e) { + return null; + } + } + + /// Private method to select first available route with stops + Future _selectFirstAvailableRoute() async { + for (final route in _allRoutes) { + try { + final stops = + await _transportationService.getRouteStopsOrderedBySeq(route.id); + if (stops.isNotEmpty) { + _selectedRouteId = route.id; + _selectedRouteName = route.displayName; + debugPrint( + '✅ Auto-selected route: ${route.id} - ${route.displayName}'); + return; + } + } catch (e) { + debugPrint('⚠️ Error checking stops for route ${route.id}: $e'); + } + } + + // Fallback: select first route even if no stops + if (_allRoutes.isNotEmpty) { + final firstRoute = _allRoutes.first; + _selectedRouteId = firstRoute.id; + _selectedRouteName = firstRoute.displayName; + debugPrint( + '✅ Fallback: selected first route: ${firstRoute.id} - ${firstRoute.displayName}'); + } + } + + /// Refresh routes and maintain selection if possible + Future refreshRoutes() async { + final previousSelectedId = _selectedRouteId; + await loadRoutes(); + + // Try to restore previous selection + if (previousSelectedId != null && + _allRoutes.any((r) => r.id == previousSelectedId)) { + await selectRoute(previousSelectedId); + } + } + + /// Get route by ID + RouteModel? getRouteById(String routeId) { + try { + return _allRoutes.firstWhere((r) => r.id == routeId); + } catch (e) { + return null; + } + } + + /// Check if route exists + bool hasRoute(String routeId) { + return _allRoutes.any((r) => r.id == routeId); + } + + /// Show toast message for no routes found + void showNoRoutesToast(BuildContext context) { + if (_allRoutes.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('No routes found'), + backgroundColor: Colors.orange, + duration: Duration(seconds: 3), + ), + ); + } + } +} \ No newline at end of file diff --git a/old/lib/services/coupon_service.dart b/old/lib/services/coupon_service.dart new file mode 100644 index 0000000..bb49be6 --- /dev/null +++ b/old/lib/services/coupon_service.dart @@ -0,0 +1,130 @@ +import '../models/coupon_model.dart'; +import './supabase_service.dart'; + +class CouponService { + /// Fetches coupons with auto-reconnection and exact query logic from requirements + static Future> getCoupons({ + String? selectedCategory, + String? sort, + }) async { + return await SupabaseService.withAutoReconnect(() async { + final client = SupabaseService.client; + + // Exact query from requirements specification + var query = client + .from('coupons') + .select( + 'id, business_name, title, description, valid_until, image_url, category, is_active, created_at', + ); + + // Apply the exact WHERE conditions from requirements + query = query.eq('is_active', true); + + // Handle date filter: (valid_until IS NULL OR valid_until >= CURRENT_DATE) + final currentDate = DateTime.now().toIso8601String().split('T')[0]; + query = query.or('valid_until.is.null,valid_until.gte.$currentDate'); + + // Category filter: ignore when 'Todos' is selected + if (selectedCategory != null && selectedCategory != 'Todos') { + final categoryValue = _mapDisplayCategoryToDbValue(selectedCategory); + query = query.eq('category', categoryValue); + } + + // Apply sorting based on requirements + final dynamic response; + if (sort == 'Por vencer') { + // Sort by expiring first (null values last) + response = await query.order('valid_until', ascending: true, nullsFirst: false); + } else { + // Default: "Más recientes" - sort by created_at descending + response = await query.order('created_at', ascending: false); + } + + return (response as List) + .map((data) => CouponModel.fromMap(data)) + .toList(); + }); + } + + /// Maps display category names to database enum values + static String _mapDisplayCategoryToDbValue(String displayCategory) { + switch (displayCategory.toLowerCase()) { + case 'restaurantes': + return 'restaurantes'; + case 'tiendas': + return 'tiendas'; + case 'servicios': + return 'servicios'; + case 'entretenimiento': + return 'entretenimiento'; + case 'salud': + return 'salud'; + case 'belleza': + return 'belleza'; + default: + return 'restaurantes'; // fallback + } + } + + /// Gets available category options for filtering (Spanish UI) + static List getCategoryOptions() { + return [ + 'Todos', + 'Restaurantes', + 'Tiendas', + 'Servicios', + 'Entretenimiento', + 'Salud', + 'Belleza', + ]; + } + + /// Gets available sort options (Spanish UI) + static List getSortOptions() { + return ['Más recientes', 'Por vencer']; + } + + /// Gets a single coupon by ID with auto-reconnection + static Future getCouponById(String id) async { + try { + return await SupabaseService.withAutoReconnect(() async { + final client = SupabaseService.client; + final response = + await client + .from('coupons') + .select( + 'id, business_name, title, description, valid_until, image_url, category, is_active, created_at', + ) + .eq('id', id) + .eq('is_active', true) + .single(); + + return CouponModel.fromMap(response); + }); + } catch (e) { + // Silent failure - return null + return null; + } + } + + /// Counts total coupons for a category with auto-reconnection + static Future getCouponCount({String? category}) async { + try { + return await SupabaseService.withAutoReconnect(() async { + final client = SupabaseService.client; + var query = client.from('coupons').select('id').eq('is_active', true); + + if (category != null && category.toLowerCase() != 'todos') { + final categoryValue = _mapDisplayCategoryToDbValue(category); + query = query.eq('category', categoryValue); + } + + final response = await query.count(); + return response.count ?? 0; + }); + } catch (e) { + // Silent failure - return 0 + return 0; + } + } +} \ No newline at end of file diff --git a/old/lib/services/supabase_service.dart b/old/lib/services/supabase_service.dart new file mode 100644 index 0000000..aec425e --- /dev/null +++ b/old/lib/services/supabase_service.dart @@ -0,0 +1,255 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +/// Enhanced Supabase service with auto-connection and silent retry logic +class SupabaseService { + static SupabaseService? _instance; + static SupabaseService get instance => _instance ??= SupabaseService._(); + + static Timer? _retryTimer; + static bool _isInitializing = false; + + SupabaseService._(); + + /// Get the Supabase client instance with auto-initialization + static SupabaseClient get client { + _ensureInitialized(); + return Supabase.instance.client; + } + + /// Auto-initialize with environment variables and silent retry + static void _ensureInitialized() { + if (!_isInitialized() && !_isInitializing) { + _initializeWithRetry(); + } + } + + /// Check if Supabase is properly initialized + static bool _isInitialized() { + try { + final url = const String.fromEnvironment('SUPABASE_URL'); + final key = const String.fromEnvironment('SUPABASE_ANON_KEY'); + + if (url.isEmpty || key.isEmpty) { + return false; + } + + // Check if current client matches the environment variables + final currentClient = Supabase.instance.client; + return currentClient.headers['Authorization']?.contains(key) ?? false; + } catch (e) { + return false; + } + } + + /// Initialize with automatic retry every 2 seconds until success + static void _initializeWithRetry() { + if (_isInitializing) return; + + _isInitializing = true; + _retryTimer?.cancel(); + + _retryTimer = Timer.periodic(const Duration(seconds: 2), (timer) async { + try { + const supabaseUrl = String.fromEnvironment('SUPABASE_URL'); + const supabaseAnonKey = String.fromEnvironment('SUPABASE_ANON_KEY'); + + // Stop and reinitialize once envs are available + if (supabaseUrl.isNotEmpty && supabaseAnonKey.isNotEmpty) { + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseAnonKey, + debug: false, + ); + + timer.cancel(); + _isInitializing = false; + _retryTimer = null; + } + } catch (e) { + // Silent retry - continue timer + } + }); + } + + /// Initialize Supabase with environment variables + static Future initialize() async { + const supabaseUrl = String.fromEnvironment('SUPABASE_URL'); + const supabaseAnonKey = String.fromEnvironment('SUPABASE_ANON_KEY'); + + if (supabaseUrl.isEmpty || supabaseAnonKey.isEmpty) { + throw Exception('Missing Supabase credentials'); + } + + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseAnonKey, + debug: false, + ); + } + + /// Auto-reconnection wrapper for database operations + static Future withAutoReconnect(Future Function() operation) async { + try { + return await operation(); + } catch (e) { + final errorMessage = e.toString().toLowerCase(); + + // Check for connection errors + if (errorMessage.contains('failed to connect') || + errorMessage.contains('networkerror') || + errorMessage.contains('invalid key') || + errorMessage.contains('connection') || + errorMessage.contains('timeout')) { + // Wait 500-1000ms debounce before retry + await Future.delayed(const Duration(milliseconds: 750)); + + try { + // Reinitialize client + await initialize(); + + // Retry the operation once + return await operation(); + } catch (retryError) { + // Silent failure - operation will return empty results + throw retryError; + } + } + + // Re-throw non-connection errors + throw e; + } + } + + /// Validate Supabase credentials and show toast if missing + static bool validateCredentials(BuildContext? context) { + const supabaseUrl = String.fromEnvironment('SUPABASE_URL'); + const supabaseAnonKey = String.fromEnvironment('SUPABASE_ANON_KEY'); + + if (supabaseUrl.isEmpty || supabaseAnonKey.isEmpty) { + Fluttertoast.showToast( + msg: "Missing SUPABASE_URL or SUPABASE_ANON_KEY", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); + return false; + } + return true; + } + + /// Get truncated Supabase URL for debug display (first/last 8 chars) + static String getTruncatedUrl() { + const supabaseUrl = String.fromEnvironment('SUPABASE_URL'); + if (supabaseUrl.isEmpty) return 'Not configured'; + if (supabaseUrl.length <= 16) return supabaseUrl; + + final first8 = supabaseUrl.substring(0, 8); + final last8 = supabaseUrl.substring(supabaseUrl.length - 8); + return '$first8...$last8'; + } + + /// Get masked Supabase anon key for debug display (first/last 6 chars, masked middle) + static String getMaskedAnonKey() { + const supabaseAnonKey = String.fromEnvironment('SUPABASE_ANON_KEY'); + if (supabaseAnonKey.isEmpty) return 'Not configured'; + if (supabaseAnonKey.length <= 12) + return '${supabaseAnonKey.substring(0, 3)}***${supabaseAnonKey.substring(supabaseAnonKey.length - 3)}'; + + final first6 = supabaseAnonKey.substring(0, 6); + final last6 = supabaseAnonKey.substring(supabaseAnonKey.length - 6); + return '$first6***$last6'; + } + + /// Connection check with lightweight test query + static Future> performConnectionCheck() async { + try { + // 1) Read env vars and validate + if (!validateCredentials(null)) { + return { + 'success': false, + 'error': 'Missing SUPABASE_URL or SUPABASE_ANON_KEY', + 'count': null, + }; + } + + // 2) Initialize Supabase client if needed + try { + await initialize(); + } catch (e) { + // Client might already be initialized, continue + } + + // 3) Run lightweight test query exactly as specified: + // const { data, error } = await supabase.from('routes').select('id', { head: true, count: 'exact' }); + final response = await Supabase.instance.client + .from('routes') + .select('id') + .limit(1); // Using limit instead of head for Flutter supabase client + + // Get count separately for exact count + final countResponse = + await Supabase.instance.client.from('routes').select('*'); + + final count = (countResponse as List).length; + + return {'success': true, 'error': null, 'count': count}; + } catch (e) { + return {'success': false, 'error': e.toString(), 'count': null}; + } + } + + /// Check if Supabase is initialized + bool get isInitialized { + try { + return Supabase.instance.client.auth.currentUser != null || + Supabase.instance.client.auth.currentSession != null || + true; // Supabase is initialized even without user + } catch (e) { + return false; + } + } + + /// Force reconnect to Supabase + static Future forceReconnect(BuildContext? context) async { + try { + if (!validateCredentials(context)) { + return false; + } + + // Re-initialize Supabase + await initialize(); + + // Test connection with the lightweight query + final result = await performConnectionCheck(); + + if (!result['success']) { + Fluttertoast.showToast( + msg: "Connection failed: ${result['error']}", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); + return false; + } + + return true; + } catch (e) { + Fluttertoast.showToast( + msg: "Failed to connect to Supabase: ${e.toString()}", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); + return false; + } + } +} diff --git a/old/lib/services/taxi_service.dart b/old/lib/services/taxi_service.dart new file mode 100644 index 0000000..24e6877 --- /dev/null +++ b/old/lib/services/taxi_service.dart @@ -0,0 +1,243 @@ +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +import '../models/taxi_model.dart'; +import './supabase_service.dart'; + +/// Service for managing taxi data and favorites functionality +class TaxiService { + static TaxiService? _instance; + static TaxiService get instance => _instance ??= TaxiService._(); + TaxiService._(); + + final SupabaseClient _client = SupabaseService.client; + static const String _favoritesKey = 'favorite_taxi_ids'; + + /// Get distinct corregimientos from active taxis + Future> getCorregimientos() async { + try { + final response = await _client + .from('taxis') + .select('corregimiento') + .eq('is_active', true) + .order('corregimiento'); + + if (response.isEmpty) return []; + + // Extract unique corregimientos + final Set uniqueCorregimientos = {}; + for (final item in response) { + final corregimiento = item['corregimiento'] as String?; + if (corregimiento != null && corregimiento.isNotEmpty) { + uniqueCorregimientos.add(corregimiento); + } + } + + return uniqueCorregimientos.toList()..sort(); + } catch (e) { + throw Exception('Failed to fetch corregimientos: $e'); + } + } + + /// Get available shifts + List getShifts() { + return ['Day', 'Evening', 'Night']; + } + + /// Search taxis with optional corregimiento, shift, and text filters + Future> searchTaxis({ + String? selectedCorregimiento, + String? selectedShift, + String? searchText, + }) async { + try { + var query = _client.from('taxis').select('*').eq('is_active', true); + + // Apply corregimiento filter + if (selectedCorregimiento != null && selectedCorregimiento.isNotEmpty) { + query = query.eq('corregimiento', selectedCorregimiento); + } + + // Apply shift filter + if (selectedShift != null && selectedShift.isNotEmpty) { + // Convert display name to database value + String dbShift = selectedShift.toLowerCase(); + query = query.eq('shift', dbShift); + } + + // Apply text search filter + if (searchText != null && searchText.isNotEmpty) { + query = query.or('name.ilike.%$searchText%,phone.ilike.%$searchText%'); + } + + final response = await query.order('name'); + + return response + .map((json) => TaxiModel.fromJson(json)) + .toList(); + } catch (e) { + throw Exception('Failed to search taxis: $e'); + } + } + + /// Get user's favorite taxi IDs (from Supabase or local storage) + Future> getFavoriteTaxiIds() async { + try { + // Check if user is authenticated + final user = _client.auth.currentUser; + if (user != null) { + // Get favorites from Supabase + final response = await _client + .from('favorite_taxis') + .select('taxi_id') + .eq('user_id', user.id); + + return response + .map((item) => item['taxi_id'] as String) + .toList(); + } else { + // Get favorites from local storage + return await _getLocalFavorites(); + } + } catch (e) { + // Fallback to local storage if Supabase fails + return await _getLocalFavorites(); + } + } + + /// Toggle favorite status for a taxi + Future toggleFavorite(String taxiId) async { + try { + final user = _client.auth.currentUser; + + if (user != null) { + // Handle Supabase favorites + return await _toggleSupabaseFavorite(user.id, taxiId); + } else { + // Handle local storage favorites + return await _toggleLocalFavorite(taxiId); + } + } catch (e) { + // Fallback to local storage + return await _toggleLocalFavorite(taxiId); + } + } + + /// Check if taxi is favorited + Future isFavorite(String taxiId) async { + final favorites = await getFavoriteTaxiIds(); + return favorites.contains(taxiId); + } + + /// Handle Supabase favorite toggle + Future _toggleSupabaseFavorite(String userId, String taxiId) async { + try { + // Check if favorite exists + final existing = + await _client + .from('favorite_taxis') + .select('id') + .eq('user_id', userId) + .eq('taxi_id', taxiId) + .maybeSingle(); + + if (existing != null) { + // Remove favorite + await _client + .from('favorite_taxis') + .delete() + .eq('user_id', userId) + .eq('taxi_id', taxiId); + return false; + } else { + // Add favorite + await _client.from('favorite_taxis').insert({ + 'user_id': userId, + 'taxi_id': taxiId, + }); + return true; + } + } catch (e) { + throw Exception('Failed to toggle Supabase favorite: $e'); + } + } + + /// Handle local storage favorite toggle + Future _toggleLocalFavorite(String taxiId) async { + try { + final prefs = await SharedPreferences.getInstance(); + final favorites = prefs.getStringList(_favoritesKey) ?? []; + + if (favorites.contains(taxiId)) { + favorites.remove(taxiId); + await prefs.setStringList(_favoritesKey, favorites); + return false; + } else { + favorites.add(taxiId); + await prefs.setStringList(_favoritesKey, favorites); + return true; + } + } catch (e) { + throw Exception('Failed to toggle local favorite: $e'); + } + } + + /// Get favorites from local storage + Future> _getLocalFavorites() async { + try { + final prefs = await SharedPreferences.getInstance(); + return prefs.getStringList(_favoritesKey) ?? []; + } catch (e) { + return []; + } + } + + /// Clear all local favorites (for testing/reset) + Future clearLocalFavorites() async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_favoritesKey); + } catch (e) { + // Silent fail for clearing favorites + } + } + + /// Sync local favorites to Supabase when user authenticates + Future syncLocalFavoritesToSupabase() async { + try { + final user = _client.auth.currentUser; + if (user == null) return; + + final localFavorites = await _getLocalFavorites(); + if (localFavorites.isEmpty) return; + + // Get existing Supabase favorites + final supabaseFavorites = await _client + .from('favorite_taxis') + .select('taxi_id') + .eq('user_id', user.id); + + final existingIds = + supabaseFavorites + .map((item) => item['taxi_id'] as String) + .toSet(); + + // Add local favorites that don't exist in Supabase + final toAdd = localFavorites.where((id) => !existingIds.contains(id)); + + if (toAdd.isNotEmpty) { + final insertData = + toAdd + .map((taxiId) => {'user_id': user.id, 'taxi_id': taxiId}) + .toList(); + + await _client.from('favorite_taxis').insert(insertData); + } + + // Clear local favorites after successful sync + await clearLocalFavorites(); + } catch (e) { + // Silent fail for sync operation + } + } +} \ No newline at end of file diff --git a/old/lib/services/transportation_service.dart b/old/lib/services/transportation_service.dart new file mode 100644 index 0000000..e189c9a --- /dev/null +++ b/old/lib/services/transportation_service.dart @@ -0,0 +1,446 @@ +import 'dart:math' as math; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:flutter/material.dart'; +import '../models/bus_stop_model.dart'; +import '../models/route_model.dart'; +import './supabase_service.dart'; + +class TransportationService { + final SupabaseService _supabaseService = SupabaseService.instance; + + /// Determines the current schedule type based on Panama timezone + String _getCurrentScheduleType() { + // Use Panama timezone for schedule type detection + final panamaNow = DateTime.now().toUtc().add( + Duration(hours: -5), + ); // Panama is UTC-5 + final dayOfWeek = panamaNow.weekday; + + // Monday = 1, Sunday = 7 + if (dayOfWeek >= 1 && dayOfWeek <= 5) { + return 'weekday'; + } else if (dayOfWeek == 6) { + return 'saturday'; + } else { + return 'sunday'; + } + } + + // Get all available routes using exact query pattern from requirements + Future> getRoutesWithCount() async { + try { + // Use exact query: supabase.from('routes').select('id,name', { count: 'exact' }).order('name', {ascending: true}) + final response = await SupabaseService.client + .from('routes') + .select('id,name') + .order('name', ascending: true); + + final routes = + (response as List) + .map((route) => RouteModel.fromJson(route)) + .toList(); + + return {'data': routes, 'count': routes.length, 'error': null}; + } catch (e) { + print('Error fetching routes: $e'); + // Show toast on exception as specified in requirements + Fluttertoast.showToast( + msg: "Error loading routes: ${e.toString()}", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); + + return {'data': [], 'count': 0, 'error': e.toString()}; + } + } + + // Get all available routes (legacy method updated to use new pattern) + Future> getRoutes() async { + final result = await getRoutesWithCount(); + return result['data'] as List; + } + + // HEAD count request for just getting route count as specified in requirements + Future getRoutesCount() async { + try { + // Use HEAD count request: supabase.from('routes').select('*', { count: 'exact', head: true }) + final response = await SupabaseService.client.from('routes').select('*'); + + return (response as List).length; + } catch (e) { + print('Error getting routes count: $e'); + Fluttertoast.showToast( + msg: "Error getting routes count: ${e.toString()}", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); + return 0; + } + } + + // Get first available route that has stops (for default selection) + Future getFirstAvailableRouteId() async { + try { + final response = await SupabaseService.client + .from('routes') + .select('id') + .order('name') + .limit(1); + + if ((response as List).isNotEmpty) { + // Verify the route has stops + final routeId = response[0]['id']; + final stopsResponse = await SupabaseService.client + .from('route_stops') + .select('route_id') + .eq('route_id', routeId) + .limit(1); + + if ((stopsResponse as List).isNotEmpty) { + return routeId; + } + } + + return null; + } catch (e) { + print('Error getting first available route: $e'); + Fluttertoast.showToast( + msg: "Error getting default route: ${e.toString()}", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); + return null; + } + } + + // Get next bus time using Panama timezone and exact query from requirements + Future?> getNextBusTime( + String routeId, + String stopId, + ) async { + try { + // Get current schedule type automatically using Panama time + final scheduleType = _getCurrentScheduleType(); + + // Use exact "Next bus" query from requirements: + // SELECT departure_time FROM timetable + // WHERE route_id=:selectedRouteId AND schedule_type=:todayType + // AND departure_time > (now() AT TIME ZONE 'America/Panama')::time + // ORDER BY departure_time LIMIT 1 + final response = await SupabaseService.client + .from('timetable') + .select('departure_time') + .eq('route_id', routeId) + .eq('schedule_type', scheduleType) + .filter( + 'departure_time', + 'gt', + 'extract(time from (now() AT TIME ZONE \'America/Panama\'))', + ) + .order('departure_time', ascending: true) + .limit(1); + + if ((response as List).isNotEmpty) { + final nextDeparture = response[0]['departure_time']; + + // Parse the departure time + final parts = nextDeparture.split(':'); + final departureHour = int.parse(parts[0]); + final departureMinute = int.parse(parts[1]); + + // Calculate minutes until departure using Panama time + final panamaNow = DateTime.now().toUtc().add(Duration(hours: -5)); + final departureDateTime = DateTime( + panamaNow.year, + panamaNow.month, + panamaNow.day, + departureHour, + departureMinute, + ); + + int minutesUntil = departureDateTime.difference(panamaNow).inMinutes; + + // If negative, it means it's for tomorrow + if (minutesUntil < 0) { + minutesUntil = + departureDateTime + .add(const Duration(days: 1)) + .difference(panamaNow) + .inMinutes; + } + + return { + 'next_departure': nextDeparture, + 'minutes_until_arrival': minutesUntil, + 'estimated_arrival_time': departureDateTime.toIso8601String(), + 'schedule_type': scheduleType, + }; + } + + // No more buses today, check for tomorrow's first departure + final nextDayScheduleType = _getNextDayScheduleType(); + final tomorrowResponse = await SupabaseService.client + .from('timetable') + .select('departure_time') + .eq('route_id', routeId) + .eq('schedule_type', nextDayScheduleType) + .order('departure_time', ascending: true) + .limit(1); + + if ((tomorrowResponse as List).isNotEmpty) { + final firstTomorrowDeparture = tomorrowResponse[0]['departure_time']; + return { + 'next_departure': null, + 'first_tomorrow': firstTomorrowDeparture, + 'minutes_until_arrival': null, + 'message': 'No more buses today', + 'schedule_type': scheduleType, + }; + } + + return null; + } catch (e) { + print('Error getting next bus time: $e'); + Fluttertoast.showToast( + msg: "Error calculating next bus: ${e.toString()}", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); + return null; + } + } + + String _getNextDayScheduleType() { + // Use Panama timezone for next day calculation + final tomorrow = DateTime.now().toUtc().add(Duration(hours: -5, days: 1)); + final dayOfWeek = tomorrow.weekday; + + if (dayOfWeek >= 1 && dayOfWeek <= 5) { + return 'weekday'; + } else if (dayOfWeek == 6) { + return 'saturday'; + } else { + return 'sunday'; + } + } + + // Get all timetables for a route with current day's schedule_type + Future>> getRouteTimetables(String routeId) async { + try { + final scheduleType = _getCurrentScheduleType(); + + final response = await SupabaseService.client + .from('timetable') + .select('*') + .eq('route_id', routeId) + .eq('schedule_type', scheduleType) + .order('departure_time'); + + return List>.from(response as List); + } catch (e) { + print('Error fetching route timetables: $e'); + Fluttertoast.showToast( + msg: "Error loading timetables: ${e.toString()}", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); + return []; + } + } + + // Get all timetables for a specific schedule type using exact query from requirements + Future>> getRouteTimetablesByScheduleType( + String routeId, + String scheduleType, + ) async { + try { + // Use EXACT query as specified in requirements: + // SELECT departure_time FROM timetable + // WHERE route_id = :selectedRouteId AND schedule_type = :todayType + // ORDER BY departure_time + final response = await SupabaseService.client + .from('timetable') + .select('departure_time') + .eq('route_id', routeId) + .eq('schedule_type', scheduleType) + .order('departure_time'); + + return List>.from(response as List); + } catch (e) { + print('Error fetching route timetables by schedule type: $e'); + Fluttertoast.showToast( + msg: "Error loading schedule: ${e.toString()}", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); + return []; + } + } + + // Get stops for a specific route - using EXACT SQL query from requirements + Future> getRouteStopsOrderedBySeq(String routeId) async { + try { + // Use EXACT SQL query as specified in requirements: + // SELECT s.id, s.name, s.lat, s.lng, rs.seq + // FROM route_stops rs JOIN stops s ON s.id = rs.stop_id + // WHERE rs.route_id = :selectedRouteId ORDER BY rs.seq + + // REMOVED RPC CALL - using direct SQL table queries instead + final response = await SupabaseService.client + .from('route_stops') + .select(''' + seq, + stops:stop_id ( + id, + name, + lat, + lng + ) + ''') + .eq('route_id', routeId) + .order('seq', ascending: true); + + List stops = []; + for (var item in response as List) { + if (item['stops'] != null) { + final stopData = Map.from(item['stops']); + stopData['seq'] = item['seq']; + stops.add(BusStopModel.fromJson(stopData)); + } + } + + // Show "No stops found for this route" message if empty as per requirements + if (stops.isEmpty) { + Fluttertoast.showToast( + msg: "No stops found for this route", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.orange, + textColor: Colors.white, + fontSize: 16.0, + ); + } + + return stops; + } catch (e) { + print('Error fetching route stops: $e'); + + // Show Supabase connection error message as per requirements + if (e.toString().contains('connection') || + e.toString().contains('network')) { + Fluttertoast.showToast( + msg: "Could not connect to Supabase. Please check credentials.", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); + } else { + Fluttertoast.showToast( + msg: "Error loading route stops: ${e.toString()}", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); + } + rethrow; // Re-throw to let calling code handle it + } + } + + // Get route by ID + Future getRouteById(String routeId) async { + try { + final response = + await SupabaseService.client + .from('routes') + .select('*') + .eq('id', routeId) + .single(); + + return RouteModel.fromJson(response); + } catch (e) { + print('Error fetching route: $e'); + Fluttertoast.showToast( + msg: "Error loading route details: ${e.toString()}", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); + return null; + } + } + + // Get stop by ID + Future getStopById(String stopId) async { + try { + final response = + await SupabaseService.client + .from('stops') + .select('*') + .eq('id', stopId) + .single(); + + return BusStopModel.fromJson(response); + } catch (e) { + print('Error fetching stop: $e'); + Fluttertoast.showToast( + msg: "Error loading stop details: ${e.toString()}", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); + return null; + } + } + + // Simple distance calculation (Haversine formula) + double _calculateDistance( + double lat1, + double lng1, + double lat2, + double lng2, + ) { + const double earthRadius = 6371; // km + double dLat = _toRadians(lat2 - lat1); + double dLng = _toRadians(lng2 - lng1); + + double a = + math.sin(dLat / 2) * math.sin(dLat / 2) + + math.cos(_toRadians(lat1)) * + math.cos(_toRadians(lat2)) * + math.sin(dLng / 2) * + math.sin(dLng / 2); + double c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); + + return earthRadius * c; + } + + double _toRadians(double degrees) { + return degrees * (math.pi / 180); + } +} \ No newline at end of file diff --git a/old/lib/theme/app_theme.dart b/old/lib/theme/app_theme.dart new file mode 100644 index 0000000..6164778 --- /dev/null +++ b/old/lib/theme/app_theme.dart @@ -0,0 +1,652 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +/// A class that contains all theme configurations for the application. +/// Implements "Purposeful Transit Minimalism" design system with high-contrast utility colors. +class AppTheme { + AppTheme._(); + + // Core color palette - High-Contrast Utility for Panama's bright outdoor conditions + static const Color primaryBlack = Color(0xFF101820); + static const Color accentYellow = Color(0xFFFEE715); + static const Color surfaceWhite = Color(0xFFFFFFFF); + static const Color textSecondary = Color(0xFF6B7280); + static const Color successGreen = Color(0xFF10B981); + static const Color warningOrange = Color(0xFFF59E0B); + static const Color errorRed = Color(0xFFEF4444); + static const Color backgroundGray = Color(0xFFF9FAFB); + static const Color borderLight = Color(0xFFE5E7EB); + static const Color overlayDark = Color(0x1F2937CC); + + // Text emphasis colors for light theme + static const Color textHighEmphasisLight = Color(0xFF101820); // Primary black + static const Color textMediumEmphasisLight = + Color(0xFF6B7280); // Text secondary + static const Color textDisabledLight = Color(0xFFE5E7EB); // Border light + + // Text emphasis colors for dark theme + static const Color textHighEmphasisDark = Color(0xFFFFFFFF); + static const Color textMediumEmphasisDark = Color(0xFF9CA3AF); + static const Color textDisabledDark = Color(0xFF6B7280); + + // Shadow colors for subtle elevation + static const Color shadowLight = + Color(0x33000000); // 20% opacity black for 2-4dp blur + static const Color shadowDark = Color(0x33FFFFFF); // 20% opacity white + + /// Light theme - Optimized for Panama's bright outdoor conditions + static ThemeData lightTheme = ThemeData( + brightness: Brightness.light, + colorScheme: ColorScheme( + brightness: Brightness.light, + primary: primaryBlack, + onPrimary: surfaceWhite, + primaryContainer: textSecondary, + onPrimaryContainer: surfaceWhite, + secondary: accentYellow, + onSecondary: primaryBlack, + secondaryContainer: warningOrange, + onSecondaryContainer: primaryBlack, + tertiary: successGreen, + onTertiary: surfaceWhite, + tertiaryContainer: successGreen, + onTertiaryContainer: surfaceWhite, + error: errorRed, + onError: surfaceWhite, + surface: surfaceWhite, + onSurface: primaryBlack, + onSurfaceVariant: textSecondary, + outline: borderLight, + outlineVariant: borderLight, + shadow: shadowLight, + scrim: overlayDark, + inverseSurface: primaryBlack, + onInverseSurface: surfaceWhite, + inversePrimary: accentYellow, + ), + scaffoldBackgroundColor: backgroundGray, + cardColor: surfaceWhite, + dividerColor: borderLight, + + // AppBar theme - Clean, minimal header + appBarTheme: AppBarTheme( + backgroundColor: surfaceWhite, + foregroundColor: primaryBlack, + elevation: 0, + shadowColor: shadowLight, + surfaceTintColor: Colors.transparent, + titleTextStyle: GoogleFonts.roboto( + fontSize: 20, + fontWeight: FontWeight.w500, + color: primaryBlack, + ), + iconTheme: const IconThemeData( + color: primaryBlack, + size: 24, + ), + ), + + // Card theme - Clean separation without borders + cardTheme: CardThemeData( + color: surfaceWhite, + elevation: 2.0, + shadowColor: shadowLight, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + + // Bottom navigation - Persistent state with contextual badges + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: surfaceWhite, + selectedItemColor: primaryBlack, + unselectedItemColor: textSecondary, + type: BottomNavigationBarType.fixed, + elevation: 8, + selectedLabelStyle: GoogleFonts.openSans( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + unselectedLabelStyle: GoogleFonts.openSans( + fontSize: 12, + fontWeight: FontWeight.w400, + ), + ), + + // Floating action button - High contrast accent + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: accentYellow, + foregroundColor: primaryBlack, + elevation: 4.0, + shape: CircleBorder(), + ), + + // Button themes - Clear action hierarchy + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + foregroundColor: primaryBlack, + backgroundColor: accentYellow, + elevation: 0, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + textStyle: GoogleFonts.openSans( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: primaryBlack, + backgroundColor: Colors.transparent, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + side: const BorderSide(color: primaryBlack, width: 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + textStyle: GoogleFonts.openSans( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: primaryBlack, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + textStyle: GoogleFonts.openSans( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + + // Typography - Optimized for Spanish character support and mobile readability + textTheme: _buildTextTheme(isLight: true), + + // Input decoration - Clean form elements with clear focus states + inputDecorationTheme: InputDecorationTheme( + fillColor: surfaceWhite, + filled: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: const BorderSide(color: borderLight, width: 1), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: const BorderSide(color: borderLight, width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: const BorderSide(color: primaryBlack, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: const BorderSide(color: errorRed, width: 1), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: const BorderSide(color: errorRed, width: 2), + ), + labelStyle: GoogleFonts.openSans( + color: textSecondary, + fontSize: 16, + fontWeight: FontWeight.w400, + ), + hintStyle: GoogleFonts.openSans( + color: textDisabledLight, + fontSize: 16, + fontWeight: FontWeight.w400, + ), + ), + + // Interactive elements + switchTheme: SwitchThemeData( + thumbColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return accentYellow; + } + return borderLight; + }), + trackColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return primaryBlack; + } + return textSecondary; + }), + ), + + checkboxTheme: CheckboxThemeData( + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return primaryBlack; + } + return Colors.transparent; + }), + checkColor: WidgetStateProperty.all(accentYellow), + side: const BorderSide(color: borderLight, width: 1), + ), + + radioTheme: RadioThemeData( + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return primaryBlack; + } + return textSecondary; + }), + ), + + progressIndicatorTheme: const ProgressIndicatorThemeData( + color: primaryBlack, + linearTrackColor: borderLight, + ), + + sliderTheme: SliderThemeData( + activeTrackColor: primaryBlack, + thumbColor: accentYellow, + overlayColor: accentYellow.withValues(alpha: 0.2), + inactiveTrackColor: borderLight, + ), + + tabBarTheme: TabBarThemeData( + labelColor: primaryBlack, + unselectedLabelColor: textSecondary, + indicatorColor: accentYellow, + indicatorSize: TabBarIndicatorSize.tab, + labelStyle: GoogleFonts.roboto( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + unselectedLabelStyle: GoogleFonts.roboto( + fontSize: 16, + fontWeight: FontWeight.w400, + ), + ), + + tooltipTheme: TooltipThemeData( + decoration: BoxDecoration( + color: primaryBlack, + borderRadius: BorderRadius.circular(8), + ), + textStyle: GoogleFonts.openSans( + color: surfaceWhite, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + + snackBarTheme: SnackBarThemeData( + backgroundColor: primaryBlack, + contentTextStyle: GoogleFonts.openSans( + color: surfaceWhite, + fontSize: 16, + fontWeight: FontWeight.w400, + ), + actionTextColor: accentYellow, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + elevation: 4, + ), dialogTheme: DialogThemeData(backgroundColor: surfaceWhite), + ); + + /// Dark theme - Maintains high contrast for nighttime use + static ThemeData darkTheme = ThemeData( + brightness: Brightness.dark, + colorScheme: ColorScheme( + brightness: Brightness.dark, + primary: accentYellow, + onPrimary: primaryBlack, + primaryContainer: warningOrange, + onPrimaryContainer: primaryBlack, + secondary: primaryBlack, + onSecondary: surfaceWhite, + secondaryContainer: textSecondary, + onSecondaryContainer: surfaceWhite, + tertiary: successGreen, + onTertiary: primaryBlack, + tertiaryContainer: successGreen, + onTertiaryContainer: primaryBlack, + error: errorRed, + onError: surfaceWhite, + surface: primaryBlack, + onSurface: surfaceWhite, + onSurfaceVariant: textMediumEmphasisDark, + outline: textSecondary, + outlineVariant: textSecondary, + shadow: shadowDark, + scrim: overlayDark, + inverseSurface: surfaceWhite, + onInverseSurface: primaryBlack, + inversePrimary: primaryBlack, + ), + scaffoldBackgroundColor: primaryBlack, + cardColor: Color(0xFF1F2937), + dividerColor: textSecondary, + appBarTheme: AppBarTheme( + backgroundColor: primaryBlack, + foregroundColor: surfaceWhite, + elevation: 0, + shadowColor: shadowDark, + surfaceTintColor: Colors.transparent, + titleTextStyle: GoogleFonts.roboto( + fontSize: 20, + fontWeight: FontWeight.w500, + color: surfaceWhite, + ), + iconTheme: const IconThemeData( + color: surfaceWhite, + size: 24, + ), + ), + cardTheme: CardThemeData( + color: Color(0xFF1F2937), + elevation: 2.0, + shadowColor: shadowDark, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: primaryBlack, + selectedItemColor: accentYellow, + unselectedItemColor: textMediumEmphasisDark, + type: BottomNavigationBarType.fixed, + elevation: 8, + selectedLabelStyle: GoogleFonts.openSans( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + unselectedLabelStyle: GoogleFonts.openSans( + fontSize: 12, + fontWeight: FontWeight.w400, + ), + ), + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: accentYellow, + foregroundColor: primaryBlack, + elevation: 4.0, + shape: CircleBorder(), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + foregroundColor: primaryBlack, + backgroundColor: accentYellow, + elevation: 0, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + textStyle: GoogleFonts.openSans( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: accentYellow, + backgroundColor: Colors.transparent, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + side: const BorderSide(color: accentYellow, width: 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + textStyle: GoogleFonts.openSans( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: accentYellow, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + textStyle: GoogleFonts.openSans( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + textTheme: _buildTextTheme(isLight: false), + inputDecorationTheme: InputDecorationTheme( + fillColor: Color(0xFF1F2937), + filled: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: const BorderSide(color: textSecondary, width: 1), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: const BorderSide(color: textSecondary, width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: const BorderSide(color: accentYellow, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: const BorderSide(color: errorRed, width: 1), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: const BorderSide(color: errorRed, width: 2), + ), + labelStyle: GoogleFonts.openSans( + color: textMediumEmphasisDark, + fontSize: 16, + fontWeight: FontWeight.w400, + ), + hintStyle: GoogleFonts.openSans( + color: textDisabledDark, + fontSize: 16, + fontWeight: FontWeight.w400, + ), + ), + switchTheme: SwitchThemeData( + thumbColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return accentYellow; + } + return textSecondary; + }), + trackColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return primaryBlack; + } + return textDisabledDark; + }), + ), + checkboxTheme: CheckboxThemeData( + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return accentYellow; + } + return Colors.transparent; + }), + checkColor: WidgetStateProperty.all(primaryBlack), + side: const BorderSide(color: textSecondary, width: 1), + ), + radioTheme: RadioThemeData( + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return accentYellow; + } + return textMediumEmphasisDark; + }), + ), + progressIndicatorTheme: const ProgressIndicatorThemeData( + color: accentYellow, + linearTrackColor: textSecondary, + ), + sliderTheme: SliderThemeData( + activeTrackColor: accentYellow, + thumbColor: accentYellow, + overlayColor: accentYellow.withValues(alpha: 0.2), + inactiveTrackColor: textSecondary, + ), + tabBarTheme: TabBarThemeData( + labelColor: accentYellow, + unselectedLabelColor: textMediumEmphasisDark, + indicatorColor: accentYellow, + indicatorSize: TabBarIndicatorSize.tab, + labelStyle: GoogleFonts.roboto( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + unselectedLabelStyle: GoogleFonts.roboto( + fontSize: 16, + fontWeight: FontWeight.w400, + ), + ), + tooltipTheme: TooltipThemeData( + decoration: BoxDecoration( + color: surfaceWhite, + borderRadius: BorderRadius.circular(8), + ), + textStyle: GoogleFonts.openSans( + color: primaryBlack, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + snackBarTheme: SnackBarThemeData( + backgroundColor: surfaceWhite, + contentTextStyle: GoogleFonts.openSans( + color: primaryBlack, + fontSize: 16, + fontWeight: FontWeight.w400, + ), + actionTextColor: primaryBlack, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + elevation: 4, + ), dialogTheme: DialogThemeData(backgroundColor: Color(0xFF1F2937)), + ); + + /// Helper method to build text theme based on brightness + /// Uses Google Fonts for optimal Spanish character support and mobile readability + static TextTheme _buildTextTheme({required bool isLight}) { + final Color textHighEmphasis = + isLight ? textHighEmphasisLight : textHighEmphasisDark; + final Color textMediumEmphasis = + isLight ? textMediumEmphasisLight : textMediumEmphasisDark; + final Color textDisabled = isLight ? textDisabledLight : textDisabledDark; + + return TextTheme( + // Display styles - Roboto for strong presence + displayLarge: GoogleFonts.roboto( + fontSize: 57, + fontWeight: FontWeight.w700, + color: textHighEmphasis, + letterSpacing: -0.25, + ), + displayMedium: GoogleFonts.roboto( + fontSize: 45, + fontWeight: FontWeight.w700, + color: textHighEmphasis, + ), + displaySmall: GoogleFonts.roboto( + fontSize: 36, + fontWeight: FontWeight.w500, + color: textHighEmphasis, + ), + + // Headline styles - Roboto for bus route names and times + headlineLarge: GoogleFonts.roboto( + fontSize: 32, + fontWeight: FontWeight.w500, + color: textHighEmphasis, + ), + headlineMedium: GoogleFonts.roboto( + fontSize: 28, + fontWeight: FontWeight.w500, + color: textHighEmphasis, + ), + headlineSmall: GoogleFonts.roboto( + fontSize: 24, + fontWeight: FontWeight.w500, + color: textHighEmphasis, + ), + + // Title styles - Roboto for section headers + titleLarge: GoogleFonts.roboto( + fontSize: 22, + fontWeight: FontWeight.w500, + color: textHighEmphasis, + letterSpacing: 0, + ), + titleMedium: GoogleFonts.roboto( + fontSize: 16, + fontWeight: FontWeight.w500, + color: textHighEmphasis, + letterSpacing: 0.15, + ), + titleSmall: GoogleFonts.roboto( + fontSize: 14, + fontWeight: FontWeight.w500, + color: textHighEmphasis, + letterSpacing: 0.1, + ), + + // Body styles - Open Sans for extended reading + bodyLarge: GoogleFonts.openSans( + fontSize: 16, + fontWeight: FontWeight.w400, + color: textHighEmphasis, + letterSpacing: 0.5, + ), + bodyMedium: GoogleFonts.openSans( + fontSize: 14, + fontWeight: FontWeight.w400, + color: textHighEmphasis, + letterSpacing: 0.25, + ), + bodySmall: GoogleFonts.openSans( + fontSize: 12, + fontWeight: FontWeight.w400, + color: textMediumEmphasis, + letterSpacing: 0.4, + ), + + // Label styles - Inter for captions and small text + labelLarge: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w500, + color: textHighEmphasis, + letterSpacing: 0.1, + ), + labelMedium: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w500, + color: textMediumEmphasis, + letterSpacing: 0.5, + ), + labelSmall: GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.w400, + color: textDisabled, + letterSpacing: 0.5, + ), + ); + } +} diff --git a/old/lib/widgets/custom_app_bar.dart b/old/lib/widgets/custom_app_bar.dart new file mode 100644 index 0000000..11788f0 --- /dev/null +++ b/old/lib/widgets/custom_app_bar.dart @@ -0,0 +1,210 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Custom app bar implementing clean, minimal header design +/// with contextual actions for the transit app +class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { + final String title; + final List? actions; + final Widget? leading; + final bool showBackButton; + final VoidCallback? onBackPressed; + final Color? backgroundColor; + final Color? foregroundColor; + final double elevation; + final bool centerTitle; + + const CustomAppBar({ + super.key, + required this.title, + this.actions, + this.leading, + this.showBackButton = true, + this.onBackPressed, + this.backgroundColor, + this.foregroundColor, + this.elevation = 0, + this.centerTitle = true, + }); + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + + void _handleBackPress(BuildContext context) { + // Haptic feedback for back navigation + HapticFeedback.lightImpact(); + + if (onBackPressed != null) { + onBackPressed!(); + } else { + Navigator.of(context).pop(); + } + } + + Widget _buildLeading(BuildContext context) { + if (leading != null) { + return leading!; + } + + if (showBackButton && Navigator.of(context).canPop()) { + return IconButton( + icon: const Icon(Icons.arrow_back_ios_new), + onPressed: () => _handleBackPress(context), + tooltip: 'Atrás', + ); + } + + return const SizedBox.shrink(); + } + + List _buildActions(BuildContext context) { + final List actionWidgets = []; + + // Add custom actions if provided + if (actions != null) { + actionWidgets.addAll(actions!); + } + + // Add contextual actions based on current route + final currentRoute = ModalRoute.of(context)?.settings.name; + + switch (currentRoute) { + case '/map-screen': + actionWidgets.add( + IconButton( + icon: const Icon(Icons.my_location), + onPressed: () { + HapticFeedback.lightImpact(); + // Handle location centering + }, + tooltip: 'Mi ubicación', + ), + ); + actionWidgets.add( + IconButton( + icon: const Icon(Icons.filter_list), + onPressed: () { + HapticFeedback.lightImpact(); + // Handle filter options + }, + tooltip: 'Filtros', + ), + ); + break; + case '/schedules-screen': + actionWidgets.add( + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () { + HapticFeedback.lightImpact(); + // Handle schedule refresh + }, + tooltip: 'Actualizar', + ), + ); + break; + case '/coupons-screen': + actionWidgets.add( + IconButton( + icon: const Icon(Icons.qr_code_scanner), + onPressed: () { + HapticFeedback.lightImpact(); + // Handle QR code scanning + }, + tooltip: 'Escanear', + ), + ); + break; + } + + return actionWidgets; + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return AppBar( + title: Text( + title, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w500, + color: foregroundColor ?? theme.colorScheme.onSurface, + ), + ), + leading: _buildLeading(context), + actions: _buildActions(context), + backgroundColor: backgroundColor ?? theme.colorScheme.surface, + foregroundColor: foregroundColor ?? theme.colorScheme.onSurface, + elevation: elevation, + shadowColor: theme.shadowColor.withValues(alpha: 0.1), + surfaceTintColor: Colors.transparent, + centerTitle: centerTitle, + systemOverlayStyle: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: theme.brightness == Brightness.light + ? Brightness.dark + : Brightness.light, + statusBarBrightness: theme.brightness, + ), + ); + } +} + +/// Specialized app bar for map screen with location and filter controls +class CustomMapAppBar extends CustomAppBar { + const CustomMapAppBar({ + super.key, + super.title = 'Mapa de Rutas', + super.showBackButton = false, + }); +} + +/// Specialized app bar for schedules screen with refresh functionality +class CustomSchedulesAppBar extends CustomAppBar { + const CustomSchedulesAppBar({ + super.key, + super.title = 'Horarios', + }); +} + +/// Specialized app bar for coupons screen with QR scanner +class CustomCouponsAppBar extends CustomAppBar { + const CustomCouponsAppBar({ + super.key, + super.title = 'Cupones', + }); +} + +/// Specialized app bar for bus stop details with contextual actions +class CustomBusStopAppBar extends CustomAppBar { + final String stopName; + + const CustomBusStopAppBar({ + super.key, + required this.stopName, + }) : super(title: stopName); + + @override + List _buildActions(BuildContext context) { + return [ + IconButton( + icon: const Icon(Icons.favorite_border), + onPressed: () { + HapticFeedback.lightImpact(); + // Handle adding to favorites + }, + tooltip: 'Agregar a favoritos', + ), + IconButton( + icon: const Icon(Icons.share), + onPressed: () { + HapticFeedback.lightImpact(); + // Handle sharing bus stop + }, + tooltip: 'Compartir', + ), + ...super._buildActions(context), + ]; + } +} diff --git a/old/lib/widgets/custom_bottom_bar.dart b/old/lib/widgets/custom_bottom_bar.dart new file mode 100644 index 0000000..96a973b --- /dev/null +++ b/old/lib/widgets/custom_bottom_bar.dart @@ -0,0 +1,202 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Custom bottom navigation bar implementing adaptive tab persistence +/// with contextual badge indicators for the transit app +class CustomBottomBar extends StatefulWidget { + final int currentIndex; + final Function(int) onTap; + + const CustomBottomBar({ + super.key, + required this.currentIndex, + required this.onTap, + }); + + @override + State createState() => _CustomBottomBarState(); +} + +class _CustomBottomBarState extends State { + // Navigation items with routes and icons + final List _navigationItems = [ + const BottomNavigationBarItem( + icon: Icon(Icons.map_outlined), + activeIcon: Icon(Icons.map), + label: 'Mapa', + ), + const BottomNavigationBarItem( + icon: Icon(Icons.schedule_outlined), + activeIcon: Icon(Icons.schedule), + label: 'Horarios', + ), + const BottomNavigationBarItem( + icon: Icon(Icons.local_offer_outlined), + activeIcon: Icon(Icons.local_offer), + label: 'Cupones', + ), + const BottomNavigationBarItem( + icon: Icon(Icons.local_taxi_outlined), + activeIcon: Icon(Icons.local_taxi), + label: 'Taxi', + ), + ]; + + final List _routes = [ + '/map-screen', + '/schedules-screen', + '/coupons-screen', + '/taxi-screen', + ]; + + void _handleTap(int index) { + if (index != widget.currentIndex) { + // Haptic feedback for tab selection + HapticFeedback.lightImpact(); + + // Navigate to selected route + Navigator.pushNamed(context, _routes[index]); + + // Call the onTap callback + widget.onTap(index); + } + } + + Widget _buildNavigationItem(BottomNavigationBarItem item, int index) { + final bool isSelected = index == widget.currentIndex; + final theme = Theme.of(context); + + return Expanded( + child: InkWell( + onTap: () => _handleTap(index), + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Icon with micro-interaction confirmation + AnimatedScale( + scale: isSelected ? 1.1 : 1.0, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isSelected + ? theme.colorScheme.secondary.withValues(alpha: 0.1) + : Colors.transparent, + borderRadius: BorderRadius.circular(12), + ), + child: _buildIconWithBadge( + isSelected ? item.activeIcon : item.icon, + index, + isSelected, + ), + ), + ), + const SizedBox(height: 4), + // Label with smooth transition + AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 200), + style: theme.textTheme.labelSmall!.copyWith( + color: isSelected + ? theme.colorScheme.primary + : theme.colorScheme.onSurface.withValues(alpha: 0.6), + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, + ), + child: Text(item.label!), + ), + ], + ), + ), + ), + ); + } + + Widget _buildIconWithBadge(Widget icon, int index, bool isSelected) { + final theme = Theme.of(context); + + // Smart notification badges - contextual indicators + bool showBadge = false; + String? badgeText; + + switch (index) { + case 0: // Map + // Show badge when there are nearby bus updates + showBadge = false; // Would be dynamic based on app state + break; + case 1: // Schedules + // Show badge when there are schedule changes + showBadge = false; // Would be dynamic based on app state + break; + case 2: // Coupons + // Show badge when there are new coupons available + showBadge = true; // Example: new coupons available + badgeText = '3'; + break; + case 3: // Taxi + // Show badge for new taxi services or favorites + showBadge = false; // Would be dynamic based on app state + break; + } + + if (!showBadge) { + return IconTheme( + data: IconThemeData( + color: isSelected + ? theme.colorScheme.primary + : theme.colorScheme.onSurface.withValues(alpha: 0.6), + size: 24, + ), + child: icon, + ); + } + + return Badge( + label: badgeText != null ? Text(badgeText) : null, + backgroundColor: theme.colorScheme.error, + textColor: theme.colorScheme.onError, + smallSize: badgeText == null ? 8 : null, + child: IconTheme( + data: IconThemeData( + color: isSelected + ? theme.colorScheme.primary + : theme.colorScheme.onSurface.withValues(alpha: 0.6), + size: 24, + ), + child: icon, + ), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + decoration: BoxDecoration( + color: theme.colorScheme.surface, + boxShadow: [ + BoxShadow( + color: theme.shadowColor.withValues(alpha: 0.1), + blurRadius: 8, + offset: const Offset(0, -2), + ), + ], + ), + child: SafeArea( + child: Container( + height: 72, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: List.generate( + _navigationItems.length, + (index) => _buildNavigationItem(_navigationItems[index], index), + ), + ), + ), + ), + ); + } +} diff --git a/old/lib/widgets/custom_error_widget.dart b/old/lib/widgets/custom_error_widget.dart new file mode 100644 index 0000000..bfd1594 --- /dev/null +++ b/old/lib/widgets/custom_error_widget.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../core/app_export.dart'; + +// custom_error_widget.dart + +class CustomErrorWidget extends StatelessWidget { + final FlutterErrorDetails? errorDetails; + final String? errorMessage; + + const CustomErrorWidget({ + Key? key, + this.errorDetails, + this.errorMessage, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFFAFAFA), + body: SafeArea( + child: Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SvgPicture.asset( + 'assets/images/sad_face.svg', + height: 42, + width: 42, + ), + const SizedBox(height: 8), + Text( + "Something went wrong", + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w500, + color: Color(0xFF262626), + ), + ), + const SizedBox(height: 4), + SizedBox( + child: const Text( + 'We encountered an unexpected error while processing your request.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Color(0xFF525252), // neutral-600 + ), + ), + ), + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: () { + bool canBeBack = Navigator.canPop(context); + if (canBeBack) { + Navigator.of(context).pop(); + } else { + Navigator.pushNamed(context, AppRoutes.initial); + } + }, + icon: + const Icon(Icons.arrow_back, size: 18, color: Colors.white), + label: const Text('Back'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.lightTheme.primaryColor, + foregroundColor: Colors.white, + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], + ), + ), + )), + ); + } +} diff --git a/old/lib/widgets/custom_icon_widget.dart b/old/lib/widgets/custom_icon_widget.dart new file mode 100644 index 0000000..9935964 --- /dev/null +++ b/old/lib/widgets/custom_icon_widget.dart @@ -0,0 +1,2189 @@ +import 'package:flutter/material.dart'; + +class CustomIconWidget extends StatelessWidget { + final String iconName; + final double size; + final Color? color; + + const CustomIconWidget( + {Key? key, required this.iconName, this.size = 24, this.color}) + : super(key: key); + + @override + Widget build(BuildContext context) { + // Map of available icons + final Map iconMap = { + // A + 'abc': Icons.abc, + 'ac_unit': Icons.ac_unit, + 'access_alarm': Icons.access_alarm, + 'access_alarms': Icons.access_alarms, + 'access_time': Icons.access_time, + 'access_time_filled': Icons.access_time_filled, + 'access_time_rounded': Icons.access_time_rounded, + 'accessibility': Icons.accessibility, + 'accessibility_new': Icons.accessibility_new, + 'accessible': Icons.accessible, + 'accessible_forward': Icons.accessible_forward, + 'account_balance': Icons.account_balance, + 'account_balance_wallet': Icons.account_balance_wallet, + 'account_box': Icons.account_box, + 'account_circle': Icons.account_circle, + 'account_tree': Icons.account_tree, + 'ad_units': Icons.ad_units, + 'adb': Icons.adb, + 'add': Icons.add, + 'add_a_photo': Icons.add_a_photo, + 'add_alarm': Icons.add_alarm, + 'add_alert': Icons.add_alert, + 'add_box': Icons.add_box, + 'add_business': Icons.add_business, + 'add_call': Icons.add_call, + 'add_card': Icons.add_card, + 'add_chart': Icons.add_chart, + 'add_circle': Icons.add_circle, + 'add_circle_outline': Icons.add_circle_outline, + 'add_comment': Icons.add_comment, + 'add_home': Icons.add_home, + 'add_home_work': Icons.add_home_work, + 'add_ic_call': Icons.add_ic_call, + 'add_link': Icons.add_link, + 'add_location': Icons.add_location, + 'add_location_alt': Icons.add_location_alt, + 'add_moderator': Icons.add_moderator, + 'add_photo_alternate': Icons.add_photo_alternate, + 'add_reaction': Icons.add_reaction, + 'add_road': Icons.add_road, + 'add_shopping_cart': Icons.add_shopping_cart, + 'add_task': Icons.add_task, + 'add_to_drive': Icons.add_to_drive, + 'add_to_home_screen': Icons.add_to_home_screen, + 'add_to_photos': Icons.add_to_photos, + 'add_to_queue': Icons.add_to_queue, + 'addchart': Icons.addchart, + 'adjust': Icons.adjust, + 'admin_panel_settings': Icons.admin_panel_settings, + 'adobe': Icons.adobe, + 'ads_click': Icons.ads_click, + 'agriculture': Icons.agriculture, + 'air': Icons.air, + 'airline_seat_flat': Icons.airline_seat_flat, + 'airline_seat_flat_angled': Icons.airline_seat_flat_angled, + 'airline_seat_individual_suite': Icons.airline_seat_individual_suite, + 'airline_seat_legroom_extra': Icons.airline_seat_legroom_extra, + 'airline_seat_legroom_normal': Icons.airline_seat_legroom_normal, + 'airline_seat_legroom_reduced': Icons.airline_seat_legroom_reduced, + 'airline_seat_recline_extra': Icons.airline_seat_recline_extra, + 'airline_seat_recline_normal': Icons.airline_seat_recline_normal, + 'airline_stops': Icons.airline_stops, + 'airlines': Icons.airlines, + 'airplane_ticket': Icons.airplane_ticket, + 'airplanemode_active': Icons.airplanemode_active, + 'airplanemode_inactive': Icons.airplanemode_inactive, + 'airplay': Icons.airplay, + 'airport_shuttle': Icons.airport_shuttle, + 'alarm': Icons.alarm, + 'alarm_add': Icons.alarm_add, + 'alarm_off': Icons.alarm_off, + 'alarm_on': Icons.alarm_on, + 'album': Icons.album, + 'align_horizontal_center': Icons.align_horizontal_center, + 'align_horizontal_left': Icons.align_horizontal_left, + 'align_horizontal_right': Icons.align_horizontal_right, + 'align_vertical_bottom': Icons.align_vertical_bottom, + 'align_vertical_center': Icons.align_vertical_center, + 'align_vertical_top': Icons.align_vertical_top, + 'all_inbox': Icons.all_inbox, + 'all_inclusive': Icons.all_inclusive, + 'all_out': Icons.all_out, + 'alt_route': Icons.alt_route, + 'alternate_email': Icons.alternate_email, + 'analytics': Icons.analytics, + 'anchor': Icons.anchor, + 'android': Icons.android, + 'animation': Icons.animation, + 'announcement': Icons.announcement, + 'aod': Icons.aod, + 'apartment': Icons.apartment, + 'api': Icons.api, + 'app_blocking': Icons.app_blocking, + 'app_registration': Icons.app_registration, + 'app_settings_alt': Icons.app_settings_alt, + 'app_shortcut': Icons.app_shortcut, + 'approval': Icons.approval, + 'apps': Icons.apps, + 'apps_outage': Icons.apps_outage, + 'architecture': Icons.architecture, + 'archive': Icons.archive, + 'area_chart': Icons.area_chart, + 'arrow_back': Icons.arrow_back, + 'arrow_back_ios': Icons.arrow_back_ios, + 'arrow_back_ios_new': Icons.arrow_back_ios_new, + 'arrow_circle_down': Icons.arrow_circle_down, + 'arrow_circle_left': Icons.arrow_circle_left, + 'arrow_circle_right': Icons.arrow_circle_right, + 'arrow_circle_up': Icons.arrow_circle_up, + 'arrow_downward': Icons.arrow_downward, + 'arrow_drop_down': Icons.arrow_drop_down, + 'arrow_drop_down_circle': Icons.arrow_drop_down_circle, + 'arrow_drop_up': Icons.arrow_drop_up, + 'arrow_forward': Icons.arrow_forward, + 'arrow_forward_ios': Icons.arrow_forward_ios, + 'arrow_forward_rounded': Icons.arrow_forward_rounded, + 'arrow_left': Icons.arrow_left, + 'arrow_outward': Icons.arrow_outward, + 'arrow_right': Icons.arrow_right, + 'arrow_right_alt': Icons.arrow_right_alt, + 'arrow_upward': Icons.arrow_upward, + 'art_track': Icons.art_track, + 'article': Icons.article, + 'aspect_ratio': Icons.aspect_ratio, + 'assessment': Icons.assessment, + 'assignment': Icons.assignment, + 'assignment_ind': Icons.assignment_ind, + 'assignment_late': Icons.assignment_late, + 'assignment_return': Icons.assignment_return, + 'assignment_returned': Icons.assignment_returned, + 'assignment_turned_in': Icons.assignment_turned_in, + 'assist_walker': Icons.assist_walker, + 'assistant': Icons.assistant, + 'assistant_direction': Icons.assistant_direction, + 'assistant_photo': Icons.assistant_photo, + 'assured_workload': Icons.assured_workload, + 'atm': Icons.atm, + 'attach_email': Icons.attach_email, + 'attach_file': Icons.attach_file, + 'attach_money': Icons.attach_money, + 'attachment': Icons.attachment, + 'attractions': Icons.attractions, + 'attribution': Icons.attribution, + 'audio_file': Icons.audio_file, + 'audiotrack': Icons.audiotrack, + 'auto_awesome': Icons.auto_awesome, + 'auto_awesome_mosaic': Icons.auto_awesome_mosaic, + 'auto_awesome_motion': Icons.auto_awesome_motion, + 'auto_delete': Icons.auto_delete, + 'auto_fix_high': Icons.auto_fix_high, + 'auto_fix_normal': Icons.auto_fix_normal, + 'auto_fix_off': Icons.auto_fix_off, + 'auto_graph': Icons.auto_graph, + 'auto_mode': Icons.auto_mode, + 'auto_stories': Icons.auto_stories, + 'autofps_select': Icons.autofps_select, + 'autorenew': Icons.autorenew, + 'av_timer': Icons.av_timer, + + // B + 'baby_changing_station': Icons.baby_changing_station, + 'back_hand': Icons.back_hand, + 'backpack': Icons.backpack, + 'backspace': Icons.backspace, + 'backup': Icons.backup, + 'backup_table': Icons.backup_table, + 'badge': Icons.badge, + 'bakery_dining': Icons.bakery_dining, + 'balance': Icons.balance, + 'balcony': Icons.balcony, + 'ballot': Icons.ballot, + 'bar_chart': Icons.bar_chart, + 'batch_prediction': Icons.batch_prediction, + 'bathroom': Icons.bathroom, + 'bathtub': Icons.bathtub, + 'battery_0_bar': Icons.battery_0_bar, + 'battery_1_bar': Icons.battery_1_bar, + 'battery_2_bar': Icons.battery_2_bar, + 'battery_3_bar': Icons.battery_3_bar, + 'battery_4_bar': Icons.battery_4_bar, + 'battery_5_bar': Icons.battery_5_bar, + 'battery_6_bar': Icons.battery_6_bar, + 'battery_alert': Icons.battery_alert, + 'battery_charging_full': Icons.battery_charging_full, + 'battery_full': Icons.battery_full, + 'battery_saver': Icons.battery_saver, + 'battery_std': Icons.battery_std, + 'battery_unknown': Icons.battery_unknown, + 'beach_access': Icons.beach_access, + 'bed': Icons.bed, + 'bedroom_baby': Icons.bedroom_baby, + 'bedroom_child': Icons.bedroom_child, + 'bedroom_parent': Icons.bedroom_parent, + 'bedtime': Icons.bedtime, + 'bedtime_off': Icons.bedtime_off, + 'beenhere': Icons.beenhere, + 'bento': Icons.bento, + 'bike_scooter': Icons.bike_scooter, + 'biotech': Icons.biotech, + 'blender': Icons.blender, + 'blind': Icons.blind, + 'blinds': Icons.blinds, + 'blinds_closed': Icons.blinds_closed, + 'block': Icons.block, + 'bloodtype': Icons.bloodtype, + 'bluetooth': Icons.bluetooth, + 'bluetooth_audio': Icons.bluetooth_audio, + 'bluetooth_connected': Icons.bluetooth_connected, + 'bluetooth_disabled': Icons.bluetooth_disabled, + 'bluetooth_drive': Icons.bluetooth_drive, + 'bluetooth_searching': Icons.bluetooth_searching, + 'blur_circular': Icons.blur_circular, + 'blur_linear': Icons.blur_linear, + 'blur_off': Icons.blur_off, + 'blur_on': Icons.blur_on, + 'bolt': Icons.bolt, + 'book': Icons.book, + 'book_online': Icons.book_online, + 'bookmark': Icons.bookmark, + 'bookmark_add': Icons.bookmark_add, + 'bookmark_added': Icons.bookmark_added, + 'bookmark_border': Icons.bookmark_border, + 'bookmark_outline': Icons.bookmark_outline, + 'bookmark_remove': Icons.bookmark_remove, + 'bookmarks': Icons.bookmarks, + 'border_all': Icons.border_all, + 'border_bottom': Icons.border_bottom, + 'border_clear': Icons.border_clear, + 'border_color': Icons.border_color, + 'border_horizontal': Icons.border_horizontal, + 'border_inner': Icons.border_inner, + 'border_left': Icons.border_left, + 'border_outer': Icons.border_outer, + 'border_right': Icons.border_right, + 'border_style': Icons.border_style, + 'border_top': Icons.border_top, + 'border_vertical': Icons.border_vertical, + 'boy': Icons.boy, + 'branding_watermark': Icons.branding_watermark, + 'breakfast_dining': Icons.breakfast_dining, + 'brightness_1': Icons.brightness_1, + 'brightness_2': Icons.brightness_2, + 'brightness_3': Icons.brightness_3, + 'brightness_4': Icons.brightness_4, + 'brightness_5': Icons.brightness_5, + 'brightness_6': Icons.brightness_6, + 'brightness_7': Icons.brightness_7, + 'brightness_auto': Icons.brightness_auto, + 'brightness_high': Icons.brightness_high, + 'brightness_low': Icons.brightness_low, + 'brightness_medium': Icons.brightness_medium, + 'broken_image': Icons.broken_image, + 'browse_gallery': Icons.browse_gallery, + 'browser_not_supported': Icons.browser_not_supported, + 'browser_updated': Icons.browser_updated, + 'brunch_dining': Icons.brunch_dining, + 'brush': Icons.brush, + 'bubble_chart': Icons.bubble_chart, + 'bug_report': Icons.bug_report, + 'build': Icons.build, + 'build_circle': Icons.build_circle, + 'bungalow': Icons.bungalow, + 'burst_mode': Icons.burst_mode, + 'bus_alert': Icons.bus_alert, + 'business': Icons.business, + 'business_center': Icons.business_center, + + // C + 'cabin': Icons.cabin, + 'cable': Icons.cable, + 'cached': Icons.cached, + 'cake': Icons.cake, + 'calculate': Icons.calculate, + 'calendar_month': Icons.calendar_month, + 'calendar_today': Icons.calendar_today, + 'calendar_view_day': Icons.calendar_view_day, + 'calendar_view_month': Icons.calendar_view_month, + 'calendar_view_week': Icons.calendar_view_week, + 'call': Icons.call, + 'call_end': Icons.call_end, + 'call_made': Icons.call_made, + 'call_merge': Icons.call_merge, + 'call_missed': Icons.call_missed, + 'call_missed_outgoing': Icons.call_missed_outgoing, + 'call_received': Icons.call_received, + 'call_split': Icons.call_split, + 'call_to_action': Icons.call_to_action, + 'camera': Icons.camera, + 'camera_alt': Icons.camera_alt, + 'camera_enhance': Icons.camera_enhance, + 'camera_front': Icons.camera_front, + 'camera_indoor': Icons.camera_indoor, + 'camera_outdoor': Icons.camera_outdoor, + 'camera_rear': Icons.camera_rear, + 'camera_roll': Icons.camera_roll, + 'cameraswitch': Icons.cameraswitch, + 'campaign': Icons.campaign, + 'cancel': Icons.cancel, + 'cancel_presentation': Icons.cancel_presentation, + 'cancel_schedule_send': Icons.cancel_schedule_send, + 'candlestick_chart': Icons.candlestick_chart, + 'car_crash': Icons.car_crash, + 'car_rental': Icons.car_rental, + 'car_repair': Icons.car_repair, + 'card_giftcard': Icons.card_giftcard, + 'card_membership': Icons.card_membership, + 'card_travel': Icons.card_travel, + 'carpenter': Icons.carpenter, + 'cases': Icons.cases, + 'casino': Icons.casino, + 'cast': Icons.cast, + 'cast_connected': Icons.cast_connected, + 'cast_for_education': Icons.cast_for_education, + 'castle': Icons.castle, + 'catching_pokemon': Icons.catching_pokemon, + 'category': Icons.category, + 'celebration': Icons.celebration, + 'cell_tower': Icons.cell_tower, + 'cell_wifi': Icons.cell_wifi, + 'center_focus_strong': Icons.center_focus_strong, + 'center_focus_weak': Icons.center_focus_weak, + 'chair': Icons.chair, + 'chair_alt': Icons.chair_alt, + 'chalet': Icons.chalet, + 'change_circle': Icons.change_circle, + 'change_history': Icons.change_history, + 'charging_station': Icons.charging_station, + 'chat': Icons.chat, + 'chat_bubble': Icons.chat_bubble, + 'chat_bubble_outline': Icons.chat_bubble_outline, + 'check': Icons.check, + 'check_box': Icons.check_box, + 'check_box_outline_blank': Icons.check_box_outline_blank, + 'check_circle': Icons.check_circle, + 'check_circle_outline': Icons.check_circle_outline, + 'checklist': Icons.checklist, + 'checklist_rtl': Icons.checklist_rtl, + 'checkroom': Icons.checkroom, + 'chevron_left': Icons.chevron_left, + 'chevron_right': Icons.chevron_right, + 'child_care': Icons.child_care, + 'child_friendly': Icons.child_friendly, + 'chrome_reader_mode': Icons.chrome_reader_mode, + 'church': Icons.church, + 'circle': Icons.circle, + 'circle_notifications': Icons.circle_notifications, + 'class_': Icons.class_, + 'clean_hands': Icons.clean_hands, + 'cleaning_services': Icons.cleaning_services, + 'clear': Icons.clear, + 'clear_all': Icons.clear_all, + 'close': Icons.close, + 'close_fullscreen': Icons.close_fullscreen, + 'closed_caption': Icons.closed_caption, + 'closed_caption_disabled': Icons.closed_caption_disabled, + 'closed_caption_off': Icons.closed_caption_off, + 'cloud': Icons.cloud, + 'cloud_circle': Icons.cloud_circle, + 'cloud_done': Icons.cloud_done, + 'cloud_download': Icons.cloud_download, + 'cloud_off': Icons.cloud_off, + 'cloud_queue': Icons.cloud_queue, + 'cloud_sync': Icons.cloud_sync, + 'cloud_upload': Icons.cloud_upload, + 'co2': Icons.co2, + 'co_present': Icons.co_present, + 'code': Icons.code, + 'code_off': Icons.code_off, + 'coffee': Icons.coffee, + 'coffee_maker': Icons.coffee_maker, + 'collections': Icons.collections, + 'collections_bookmark': Icons.collections_bookmark, + 'color_lens': Icons.color_lens, + 'colorize': Icons.colorize, + 'comment': Icons.comment, + 'comment_bank': Icons.comment_bank, + 'comments_disabled': Icons.comments_disabled, + 'commit': Icons.commit, + 'commute': Icons.commute, + 'compare': Icons.compare, + 'compare_arrows': Icons.compare_arrows, + 'compass_calibration': Icons.compass_calibration, + 'compost': Icons.compost, + 'compress': Icons.compress, + 'computer': Icons.computer, + 'confirmation_num': Icons.confirmation_num, + 'confirmation_number': Icons.confirmation_number, + 'connect_without_contact': Icons.connect_without_contact, + 'connected_tv': Icons.connected_tv, + 'connecting_airports': Icons.connecting_airports, + 'construction': Icons.construction, + 'contact_emergency': Icons.contact_emergency, + 'contact_mail': Icons.contact_mail, + 'contact_page': Icons.contact_page, + 'contact_phone': Icons.contact_phone, + 'contact_support': Icons.contact_support, + 'contactless': Icons.contactless, + 'contacts': Icons.contacts, + 'content_copy': Icons.content_copy, + 'content_cut': Icons.content_cut, + 'content_paste': Icons.content_paste, + 'content_paste_go': Icons.content_paste_go, + 'content_paste_off': Icons.content_paste_off, + 'content_paste_search': Icons.content_paste_search, + 'contrast': Icons.contrast, + 'control_camera': Icons.control_camera, + 'control_point': Icons.control_point, + 'control_point_duplicate': Icons.control_point_duplicate, + 'cookie': Icons.cookie, + 'copy_all': Icons.copy_all, + 'copyright': Icons.copyright, + 'coronavirus': Icons.coronavirus, + 'corporate_fare': Icons.corporate_fare, + 'cottage': Icons.cottage, + 'countertops': Icons.countertops, + 'create': Icons.create, + 'create_new_folder': Icons.create_new_folder, + 'credit_card': Icons.credit_card, + 'credit_card_off': Icons.credit_card_off, + 'credit_score': Icons.credit_score, + 'crib': Icons.crib, + 'crisis_alert': Icons.crisis_alert, + 'crop': Icons.crop, + 'crop_16_9': Icons.crop_16_9, + 'crop_3_2': Icons.crop_3_2, + 'crop_5_4': Icons.crop_5_4, + 'crop_7_5': Icons.crop_7_5, + 'crop_din': Icons.crop_din, + 'crop_free': Icons.crop_free, + 'crop_landscape': Icons.crop_landscape, + 'crop_original': Icons.crop_original, + 'crop_portrait': Icons.crop_portrait, + 'crop_rotate': Icons.crop_rotate, + 'crop_square': Icons.crop_square, + 'cruelty_free': Icons.cruelty_free, + 'css': Icons.css, + 'currency_bitcoin': Icons.currency_bitcoin, + 'currency_exchange': Icons.currency_exchange, + 'currency_franc': Icons.currency_franc, + 'currency_lira': Icons.currency_lira, + 'currency_pound': Icons.currency_pound, + 'currency_ruble': Icons.currency_ruble, + 'currency_rupee': Icons.currency_rupee, + 'currency_yen': Icons.currency_yen, + 'currency_yuan': Icons.currency_yuan, + 'curtains': Icons.curtains, + 'curtains_closed': Icons.curtains_closed, + + // D + 'dangerous': Icons.dangerous, + 'dark_mode': Icons.dark_mode, + 'dark_mode_outlined': Icons.dark_mode_outlined, + 'dashboard': Icons.dashboard, + 'dashboard_customize': Icons.dashboard_customize, + 'data_array': Icons.data_array, + 'data_exploration': Icons.data_exploration, + 'data_object': Icons.data_object, + 'data_saver_off': Icons.data_saver_off, + 'data_saver_on': Icons.data_saver_on, + 'data_thresholding': Icons.data_thresholding, + 'data_usage': Icons.data_usage, + 'date_range': Icons.date_range, + 'deblur': Icons.deblur, + 'deck': Icons.deck, + 'dehaze': Icons.dehaze, + 'delete': Icons.delete, + 'delete_forever': Icons.delete_forever, + 'delete_outline': Icons.delete_outline, + 'delete_sweep': Icons.delete_sweep, + 'delivery_dining': Icons.delivery_dining, + 'density_large': Icons.density_large, + 'density_medium': Icons.density_medium, + 'density_small': Icons.density_small, + 'departure_board': Icons.departure_board, + 'description': Icons.description, + 'deselect': Icons.deselect, + 'design_services': Icons.design_services, + 'desk': Icons.desk, + 'desktop_access_disabled': Icons.desktop_access_disabled, + 'desktop_mac': Icons.desktop_mac, + 'desktop_windows': Icons.desktop_windows, + 'details': Icons.details, + 'developer_board': Icons.developer_board, + 'developer_board_off': Icons.developer_board_off, + 'developer_mode': Icons.developer_mode, + 'device_hub': Icons.device_hub, + 'device_thermostat': Icons.device_thermostat, + 'device_unknown': Icons.device_unknown, + 'devices': Icons.devices, + 'devices_fold': Icons.devices_fold, + 'devices_other': Icons.devices_other, + 'dew_point': Icons.dew_point, + 'dialer_sip': Icons.dialer_sip, + 'dialpad': Icons.dialpad, + 'diamond': Icons.diamond, + 'difference': Icons.difference, + 'dining': Icons.dining, + 'dinner_dining': Icons.dinner_dining, + 'directions': Icons.directions, + 'directions_bike': Icons.directions_bike, + 'directions_boat': Icons.directions_boat, + 'directions_boat_filled': Icons.directions_boat_filled, + 'directions_bus': Icons.directions_bus, + 'directions_bus_filled': Icons.directions_bus_filled, + 'directions_car': Icons.directions_car, + 'directions_car_filled': Icons.directions_car_filled, + 'directions_ferry': Icons.directions_ferry, + 'directions_off': Icons.directions_off, + 'directions_railway': Icons.directions_railway, + 'directions_railway_filled': Icons.directions_railway_filled, + 'directions_run': Icons.directions_run, + 'directions_subway': Icons.directions_subway, + 'directions_subway_filled': Icons.directions_subway_filled, + 'directions_transit': Icons.directions_transit, + 'directions_transit_filled': Icons.directions_transit_filled, + 'directions_walk': Icons.directions_walk, + 'dirty_lens': Icons.dirty_lens, + 'disabled_by_default': Icons.disabled_by_default, + 'disabled_visible': Icons.disabled_visible, + 'disc_full': Icons.disc_full, + 'discord': Icons.discord, + 'discount': Icons.discount, + 'display_settings': Icons.display_settings, + 'diversity_1': Icons.diversity_1, + 'diversity_2': Icons.diversity_2, + 'diversity_3': Icons.diversity_3, + 'dns': Icons.dns, + 'do_disturb': Icons.do_disturb, + 'do_disturb_alt': Icons.do_disturb_alt, + 'do_disturb_off': Icons.do_disturb_off, + 'do_disturb_on': Icons.do_disturb_on, + 'do_not_disturb': Icons.do_not_disturb, + 'do_not_disturb_alt': Icons.do_not_disturb_alt, + 'do_not_disturb_off': Icons.do_not_disturb_off, + 'do_not_disturb_on': Icons.do_not_disturb_on, + 'do_not_disturb_on_total_silence': Icons.do_not_disturb_on_total_silence, + 'do_not_step': Icons.do_not_step, + 'do_not_touch': Icons.do_not_touch, + 'dock': Icons.dock, + 'document_scanner': Icons.document_scanner, + 'domain': Icons.domain, + 'domain_add': Icons.domain_add, + 'domain_disabled': Icons.domain_disabled, + 'domain_verification': Icons.domain_verification, + 'done': Icons.done, + 'done_all': Icons.done_all, + 'done_outline': Icons.done_outline, + 'donut_large': Icons.donut_large, + 'donut_small': Icons.donut_small, + 'door_back': Icons.door_back_door, + 'door_front': Icons.door_front_door, + 'door_sliding': Icons.door_sliding, + 'doorbell': Icons.doorbell, + 'double_arrow': Icons.double_arrow, + 'downhill_skiing': Icons.downhill_skiing, + 'download': Icons.download, + 'download_done': Icons.download_done, + 'download_for_offline': Icons.download_for_offline, + 'downloading': Icons.downloading, + 'drafts': Icons.drafts, + 'drag_handle': Icons.drag_handle, + 'drag_indicator': Icons.drag_indicator, + 'draw': Icons.draw, + 'drive_eta': Icons.drive_eta, + 'drive_file_move': Icons.drive_file_move, + 'drive_file_move_outline': Icons.drive_file_move_outline, + 'drive_file_move_rtl': Icons.drive_file_move_rtl, + 'drive_file_rename_outline': Icons.drive_file_rename_outline, + 'drive_folder_upload': Icons.drive_folder_upload, + 'dry': Icons.dry, + 'dry_cleaning': Icons.dry_cleaning, + 'duo': Icons.duo, + 'dvr': Icons.dvr, + 'dynamic_feed': Icons.dynamic_feed, + 'dynamic_form': Icons.dynamic_form, + + // E + 'e_mobiledata': Icons.e_mobiledata, + 'earbuds': Icons.earbuds, + 'earbuds_battery': Icons.earbuds_battery, + 'east': Icons.east, + 'eco': Icons.eco, + 'edgesensor_high': Icons.edgesensor_high, + 'edgesensor_low': Icons.edgesensor_low, + 'edit': Icons.edit, + 'edit_attributes': Icons.edit_attributes, + 'edit_calendar': Icons.edit_calendar, + 'edit_document': Icons.edit_document, + 'edit_location': Icons.edit_location, + 'edit_location_alt': Icons.edit_location_alt, + 'edit_note': Icons.edit_note, + 'edit_notifications': Icons.edit_notifications, + 'edit_off': Icons.edit_off, + 'edit_outlined': Icons.edit_outlined, + 'edit_road': Icons.edit_road, + 'egg': Icons.egg, + 'egg_alt': Icons.egg_alt, + 'eject': Icons.eject, + 'elderly': Icons.elderly, + 'elderly_woman': Icons.elderly_woman, + 'electric_bike': Icons.electric_bike, + 'electric_bolt': Icons.electric_bolt, + 'electric_car': Icons.electric_car, + 'electric_meter': Icons.electric_meter, + 'electric_moped': Icons.electric_moped, + 'electric_rickshaw': Icons.electric_rickshaw, + 'electric_scooter': Icons.electric_scooter, + 'electrical_services': Icons.electrical_services, + 'elevator': Icons.elevator, + 'email': Icons.email, + 'emergency': Icons.emergency, + 'emergency_recording': Icons.emergency_recording, + 'emergency_share': Icons.emergency_share, + 'emoji_emotions': Icons.emoji_emotions, + 'emoji_events': Icons.emoji_events, + 'emoji_flags': Icons.emoji_flags, + 'emoji_food_beverage': Icons.emoji_food_beverage, + 'emoji_nature': Icons.emoji_nature, + 'emoji_objects': Icons.emoji_objects, + 'emoji_people': Icons.emoji_people, + 'emoji_symbols': Icons.emoji_symbols, + 'emoji_transportation': Icons.emoji_transportation, + 'engineering': Icons.engineering, + 'enhance_photo_translate': Icons.enhance_photo_translate, + 'enhanced_encryption': Icons.enhanced_encryption, + 'equalizer': Icons.equalizer, + 'error': Icons.error, + 'error_outline': Icons.error_outline, + 'escalator': Icons.escalator, + 'escalator_warning': Icons.escalator_warning, + 'euro': Icons.euro, + 'euro_symbol': Icons.euro_symbol, + 'ev_station': Icons.ev_station, + 'event': Icons.event, + 'event_available': Icons.event_available, + 'event_busy': Icons.event_busy, + 'event_note': Icons.event_note, + 'event_repeat': Icons.event_repeat, + 'event_seat': Icons.event_seat, + 'exit_to_app': Icons.exit_to_app, + 'expand': Icons.expand, + 'expand_circle_down': Icons.expand_circle_down, + 'expand_less': Icons.expand_less, + 'expand_more': Icons.expand_more, + 'explicit': Icons.explicit, + 'explore': Icons.explore, + 'explore_off': Icons.explore_off, + 'exposure': Icons.exposure, + 'exposure_neg_1': Icons.exposure_neg_1, + 'exposure_neg_2': Icons.exposure_neg_2, + 'exposure_plus_1': Icons.exposure_plus_1, + 'exposure_plus_2': Icons.exposure_plus_2, + 'exposure_zero': Icons.exposure_zero, + 'extension': Icons.extension, + 'extension_off': Icons.extension_off, + + // F + 'face': Icons.face, + 'face_2': Icons.face_2, + 'face_3': Icons.face_3, + 'face_4': Icons.face_4, + 'face_5': Icons.face_5, + 'face_6': Icons.face_6, + 'face_retouching_natural': Icons.face_retouching_natural, + 'face_retouching_off': Icons.face_retouching_off, + 'fact_check': Icons.fact_check, + 'factory': Icons.factory, + 'family_restroom': Icons.family_restroom, + 'fast_forward': Icons.fast_forward, + 'fast_rewind': Icons.fast_rewind, + 'fastfood': Icons.fastfood, + 'favorite': Icons.favorite, + 'favorite_border': Icons.favorite_border, + 'favorite_outline': Icons.favorite_outline, + 'fax': Icons.fax, + 'featured_play_list': Icons.featured_play_list, + 'featured_video': Icons.featured_video, + 'feed': Icons.feed, + 'feedback': Icons.feedback, + 'female': Icons.female, + 'fence': Icons.fence, + 'festival': Icons.festival, + 'fiber_dvr': Icons.fiber_dvr, + 'fiber_manual_record': Icons.fiber_manual_record, + 'fiber_new': Icons.fiber_new, + 'fiber_pin': Icons.fiber_pin, + 'fiber_smart_record': Icons.fiber_smart_record, + 'file_copy': Icons.file_copy, + 'file_download': Icons.file_download, + 'file_download_done': Icons.file_download_done, + 'file_download_off': Icons.file_download_off, + 'file_open': Icons.file_open, + 'file_present': Icons.file_present, + 'file_upload': Icons.file_upload, + 'filter': Icons.filter, + 'filter_1': Icons.filter_1, + 'filter_2': Icons.filter_2, + 'filter_3': Icons.filter_3, + 'filter_4': Icons.filter_4, + 'filter_5': Icons.filter_5, + 'filter_6': Icons.filter_6, + 'filter_7': Icons.filter_7, + 'filter_8': Icons.filter_8, + 'filter_9': Icons.filter_9, + 'filter_9_plus': Icons.filter_9_plus, + 'filter_alt': Icons.filter_alt, + 'filter_alt_off': Icons.filter_alt_off, + 'filter_b_and_w': Icons.filter_b_and_w, + 'filter_center_focus': Icons.filter_center_focus, + 'filter_drama': Icons.filter_drama, + 'filter_frames': Icons.filter_frames, + 'filter_hdr': Icons.filter_hdr, + 'filter_list': Icons.filter_list, + 'filter_list_alt': Icons.filter_list_alt, + 'filter_list_off': Icons.filter_list_off, + 'filter_none': Icons.filter_none, + 'filter_tilt_shift': Icons.filter_tilt_shift, + 'filter_vintage': Icons.filter_vintage, + 'find_in_page': Icons.find_in_page, + 'find_replace': Icons.find_replace, + 'fingerprint': Icons.fingerprint, + 'fire_extinguisher': Icons.fire_extinguisher, + 'fire_hydrant': Icons.fire_hydrant, + 'fire_truck': Icons.fire_truck, + 'fireplace': Icons.fireplace, + 'first_page': Icons.first_page, + 'fit_screen': Icons.fit_screen, + 'fitness_center': Icons.fitness_center, + 'flag': Icons.flag, + 'flag_circle': Icons.flag_circle, + 'flaky': Icons.flaky, + 'flare': Icons.flare, + 'flash_auto': Icons.flash_auto, + 'flash_off': Icons.flash_off, + 'flash_on': Icons.flash_on, + 'flashlight_off': Icons.flashlight_off, + 'flashlight_on': Icons.flashlight_on, + 'flatware': Icons.flatware, + 'flight': Icons.flight, + 'flight_class': Icons.flight_class, + 'flight_land': Icons.flight_land, + 'flight_takeoff': Icons.flight_takeoff, + 'flip': Icons.flip, + 'flip_camera_android': Icons.flip_camera_android, + 'flip_camera_ios': Icons.flip_camera_ios, + 'flip_to_back': Icons.flip_to_back, + 'flip_to_front': Icons.flip_to_front, + 'flood': Icons.flood, + 'fluorescent': Icons.fluorescent, + 'flutter_dash': Icons.flutter_dash, + 'fmd_bad': Icons.fmd_bad, + 'fmd_good': Icons.fmd_good, + 'folder': Icons.folder, + 'folder_copy': Icons.folder_copy, + 'folder_delete': Icons.folder_delete, + 'folder_off': Icons.folder_off, + 'folder_open': Icons.folder_open, + 'folder_shared': Icons.folder_shared, + 'folder_special': Icons.folder_special, + 'folder_zip': Icons.folder_zip, + 'follow_the_signs': Icons.follow_the_signs, + 'font_download': Icons.font_download, + 'font_download_off': Icons.font_download_off, + 'food_bank': Icons.food_bank, + 'forest': Icons.forest, + 'fork_left': Icons.fork_left, + 'fork_right': Icons.fork_right, + 'format_align_center': Icons.format_align_center, + 'format_align_justify': Icons.format_align_justify, + 'format_align_left': Icons.format_align_left, + 'format_align_right': Icons.format_align_right, + 'format_bold': Icons.format_bold, + 'format_clear': Icons.format_clear, + 'format_color_fill': Icons.format_color_fill, + 'format_color_reset': Icons.format_color_reset, + 'format_color_text': Icons.format_color_text, + 'format_indent_decrease': Icons.format_indent_decrease, + 'format_indent_increase': Icons.format_indent_increase, + 'format_italic': Icons.format_italic, + 'format_line_spacing': Icons.format_line_spacing, + 'format_list_bulleted': Icons.format_list_bulleted, + 'format_list_numbered': Icons.format_list_numbered, + 'format_list_numbered_rtl': Icons.format_list_numbered_rtl, + 'format_overline': Icons.format_overline, + 'format_paint': Icons.format_paint, + 'format_quote': Icons.format_quote, + 'format_shapes': Icons.format_shapes, + 'format_size': Icons.format_size, + 'format_strikethrough': Icons.format_strikethrough, + 'format_textdirection_l_to_r': Icons.format_textdirection_l_to_r, + 'format_textdirection_r_to_l': Icons.format_textdirection_r_to_l, + 'format_underlined': Icons.format_underlined, + 'forum': Icons.forum, + 'forward': Icons.forward, + 'forward_10': Icons.forward_10, + 'forward_30': Icons.forward_30, + 'forward_5': Icons.forward_5, + 'forward_to_inbox': Icons.forward_to_inbox, + 'foundation': Icons.foundation, + 'free_breakfast': Icons.free_breakfast, + 'free_cancellation': Icons.free_cancellation, + 'front_hand': Icons.front_hand, + 'fullscreen': Icons.fullscreen, + 'fullscreen_exit': Icons.fullscreen_exit, + 'functions': Icons.functions, + + // G + 'g_mobiledata': Icons.g_mobiledata, + 'g_translate': Icons.g_translate, + 'gamepad': Icons.gamepad, + 'games': Icons.games, + 'garage': Icons.garage, + 'gas_meter': Icons.gas_meter, + 'gavel': Icons.gavel, + 'generating_tokens': Icons.generating_tokens, + 'gesture': Icons.gesture, + 'get_app': Icons.get_app, + 'gif': Icons.gif, + 'gif_box': Icons.gif_box, + 'girl': Icons.girl, + 'gite': Icons.gite, + 'golf_course': Icons.golf_course, + 'gpp_bad': Icons.gpp_bad, + 'gpp_good': Icons.gpp_good, + 'gpp_maybe': Icons.gpp_maybe, + 'gps_fixed': Icons.gps_fixed, + 'gps_not_fixed': Icons.gps_not_fixed, + 'gps_off': Icons.gps_off, + 'grade': Icons.grade, + 'gradient': Icons.gradient, + 'grading': Icons.grading, + 'grain': Icons.grain, + 'graphic_eq': Icons.graphic_eq, + 'grass': Icons.grass, + 'grid_3x3': Icons.grid_3x3, + 'grid_4x4': Icons.grid_4x4, + 'grid_goldenratio': Icons.grid_goldenratio, + 'grid_off': Icons.grid_off, + 'grid_on': Icons.grid_on, + 'grid_view': Icons.grid_view, + 'group': Icons.group, + 'group_add': Icons.group_add, + 'group_off': Icons.group_off, + 'group_remove': Icons.group_remove, + 'group_work': Icons.group_work, + 'groups': Icons.groups, + 'groups_2': Icons.groups_2, + 'groups_3': Icons.groups_3, + 'h_mobiledata': Icons.h_mobiledata, + 'h_plus_mobiledata': Icons.h_plus_mobiledata, + + // H + 'hail': Icons.hail, + 'handshake': Icons.handshake, + 'handyman': Icons.handyman, + 'hardware': Icons.hardware, + 'hd': Icons.hd, + 'hdr_auto': Icons.hdr_auto, + 'hdr_auto_select': Icons.hdr_auto_select, + 'hdr_enhanced_select': Icons.hdr_enhanced_select, + 'hdr_off': Icons.hdr_off, + 'hdr_off_select': Icons.hdr_off_select, + 'hdr_on': Icons.hdr_on, + 'hdr_on_select': Icons.hdr_on_select, + 'hdr_plus': Icons.hdr_plus, + 'hdr_strong': Icons.hdr_strong, + 'hdr_weak': Icons.hdr_weak, + 'headphones': Icons.headphones, + 'headphones_battery': Icons.headphones_battery, + 'headset': Icons.headset, + 'headset_mic': Icons.headset_mic, + 'headset_off': Icons.headset_off, + 'healing': Icons.healing, + 'health_and_safety': Icons.health_and_safety, + 'hearing': Icons.hearing, + 'hearing_disabled': Icons.hearing_disabled, + 'heart_broken': Icons.heart_broken, + 'heat_pump': Icons.heat_pump, + 'height': Icons.height, + 'help': Icons.help, + 'help_center': Icons.help_center, + 'help_outline': Icons.help_outline, + 'hevc': Icons.hevc, + 'hexagon': Icons.hexagon, + 'hide_image': Icons.hide_image, + 'hide_source': Icons.hide_source, + 'high_quality': Icons.high_quality, + 'highlight': Icons.highlight, + 'highlight_alt': Icons.highlight_alt, + 'highlight_off': Icons.highlight_off, + 'highlight_remove': Icons.highlight_remove, + 'hiking': Icons.hiking, + 'history': Icons.history, + 'history_edu': Icons.history_edu, + 'history_toggle_off': Icons.history_toggle_off, + 'hive': Icons.hive, + 'hls': Icons.hls, + 'hls_off': Icons.hls_off, + 'holiday_village': Icons.holiday_village, + 'home': Icons.home, + 'home_filled': Icons.home_filled, + 'home_max': Icons.home_max, + 'home_mini': Icons.home_mini, + 'home_outlined': Icons.home_outlined, + 'home_repair_service': Icons.home_repair_service, + 'home_work': Icons.home_work, + 'horizontal_distribute': Icons.horizontal_distribute, + 'horizontal_rule': Icons.horizontal_rule, + 'horizontal_split': Icons.horizontal_split, + 'hot_tub': Icons.hot_tub, + 'hotel': Icons.hotel, + 'hotel_class': Icons.hotel_class, + 'hourglass_bottom': Icons.hourglass_bottom, + 'hourglass_disabled': Icons.hourglass_disabled, + 'hourglass_empty': Icons.hourglass_empty, + 'hourglass_full': Icons.hourglass_full, + 'hourglass_top': Icons.hourglass_top, + 'house': Icons.house, + 'house_siding': Icons.house_siding, + 'houseboat': Icons.houseboat, + 'how_to_reg': Icons.how_to_reg, + 'how_to_vote': Icons.how_to_vote, + 'html': Icons.html, + 'http': Icons.http, + 'https': Icons.https, + 'hub': Icons.hub, + 'hvac': Icons.hvac, + + // I + 'ice_skating': Icons.ice_skating, + 'icecream': Icons.icecream, + 'image': Icons.image, + 'image_aspect_ratio': Icons.image_aspect_ratio, + 'image_not_supported': Icons.image_not_supported, + 'image_search': Icons.image_search, + 'imagesearch_roller': Icons.imagesearch_roller, + 'import_contacts': Icons.import_contacts, + 'import_export': Icons.import_export, + 'important_devices': Icons.important_devices, + 'inbox': Icons.inbox, + 'incomplete_circle': Icons.incomplete_circle, + 'indeterminate_check_box': Icons.indeterminate_check_box, + 'info': Icons.info, + 'info_outline': Icons.info_outline, + 'input': Icons.input, + 'insert_chart': Icons.insert_chart, + 'insert_chart_outlined': Icons.insert_chart_outlined, + 'insert_comment': Icons.insert_comment, + 'insert_drive_file': Icons.insert_drive_file, + 'insert_emoticon': Icons.insert_emoticon, + 'insert_invitation': Icons.insert_invitation, + 'insert_link': Icons.insert_link, + 'insert_page_break': Icons.insert_page_break, + 'insert_photo': Icons.insert_photo, + 'insights': Icons.insights, + 'install_desktop': Icons.install_desktop, + 'install_mobile': Icons.install_mobile, + 'integration_instructions': Icons.integration_instructions, + 'interests': Icons.interests, + 'interpreter_mode': Icons.interpreter_mode, + 'inventory': Icons.inventory, + 'inventory_2': Icons.inventory_2, + 'invert_colors': Icons.invert_colors, + 'invert_colors_off': Icons.invert_colors_off, + 'ios_share': Icons.ios_share, + 'iron': Icons.iron, + 'iso': Icons.iso, + + // J + 'javascript': Icons.javascript, + 'join_full': Icons.join_full, + 'join_inner': Icons.join_inner, + 'join_left': Icons.join_left, + 'join_right': Icons.join_right, + 'joystick': Icons.gamepad, + 'jpeg': Icons.image, + 'kayaking': Icons.kayaking, + + // K + 'kebab_dining': Icons.kebab_dining, + 'key': Icons.key, + 'key_off': Icons.key_off, + 'keyboard': Icons.keyboard, + 'keyboard_alt': Icons.keyboard_alt, + 'keyboard_arrow_down': Icons.keyboard_arrow_down, + 'keyboard_arrow_left': Icons.keyboard_arrow_left, + 'keyboard_arrow_right': Icons.keyboard_arrow_right, + 'keyboard_arrow_up': Icons.keyboard_arrow_up, + 'keyboard_backspace': Icons.keyboard_backspace, + 'keyboard_capslock': Icons.keyboard_capslock, + 'keyboard_command_key': Icons.keyboard_command_key, + 'keyboard_control_key': Icons.keyboard_control_key, + 'keyboard_double_arrow_down': Icons.keyboard_double_arrow_down, + 'keyboard_double_arrow_left': Icons.keyboard_double_arrow_left, + 'keyboard_double_arrow_right': Icons.keyboard_double_arrow_right, + 'keyboard_double_arrow_up': Icons.keyboard_double_arrow_up, + 'keyboard_hide': Icons.keyboard_hide, + 'keyboard_option_key': Icons.keyboard_option_key, + 'keyboard_return': Icons.keyboard_return, + 'keyboard_tab': Icons.keyboard_tab, + 'keyboard_voice': Icons.keyboard_voice, + 'king_bed': Icons.king_bed, + 'kitchen': Icons.kitchen, + 'kitesurfing': Icons.kitesurfing, + + // L + 'label': Icons.label, + 'label_important': Icons.label_important, + 'label_important_outline': Icons.label_important_outline, + 'label_off': Icons.label_off, + 'label_outline': Icons.label_outline, + 'lan': Icons.lan, + 'landscape': Icons.landscape, + 'landslide': Icons.landslide, + 'language': Icons.language, + 'laptop': Icons.laptop, + 'laptop_chromebook': Icons.laptop_chromebook, + 'laptop_mac': Icons.laptop_mac, + 'laptop_windows': Icons.laptop_windows, + 'last_page': Icons.last_page, + 'launch': Icons.launch, + 'layers': Icons.layers, + 'layers_clear': Icons.layers_clear, + 'leaderboard': Icons.leaderboard, + 'leak_add': Icons.leak_add, + 'leak_remove': Icons.leak_remove, + 'legend_toggle': Icons.legend_toggle, + 'lens': Icons.lens, + 'lens_blur': Icons.lens_blur, + 'library_add': Icons.library_add, + 'library_add_check': Icons.library_add_check, + 'library_books': Icons.library_books, + 'library_music': Icons.library_music, + 'light': Icons.light, + 'light_mode': Icons.light_mode, + 'lightbulb': Icons.lightbulb, + 'lightbulb_outline': Icons.lightbulb_outline, + 'line_axis': Icons.line_axis, + 'line_style': Icons.line_style, + 'line_weight': Icons.line_weight, + 'linear_scale': Icons.linear_scale, + 'link': Icons.link, + 'link_off': Icons.link_off, + 'linked_camera': Icons.linked_camera, + 'liquor': Icons.liquor, + 'list': Icons.list, + 'list_alt': Icons.list_alt, + 'live_help': Icons.live_help, + 'live_tv': Icons.live_tv, + 'living': Icons.living, + 'local_activity': Icons.local_activity, + 'local_airport': Icons.local_airport, + 'local_atm': Icons.local_atm, + 'local_bar': Icons.local_bar, + 'local_cafe': Icons.local_cafe, + 'local_car_wash': Icons.local_car_wash, + 'local_convenience_store': Icons.local_convenience_store, + 'local_dining': Icons.local_dining, + 'local_drink': Icons.local_drink, + 'local_fire_department': Icons.local_fire_department, + 'local_fire_department_outlined': Icons.local_fire_department_outlined, + 'local_florist': Icons.local_florist, + 'local_gas_station': Icons.local_gas_station, + 'local_grocery_store': Icons.local_grocery_store, + 'local_hospital': Icons.local_hospital, + 'local_hotel': Icons.local_hotel, + 'local_laundry_service': Icons.local_laundry_service, + 'local_library': Icons.local_library, + 'local_mall': Icons.local_mall, + 'local_movies': Icons.local_movies, + 'local_offer': Icons.local_offer, + 'local_parking': Icons.local_parking, + 'local_pharmacy': Icons.local_pharmacy, + 'local_phone': Icons.local_phone, + 'local_pizza': Icons.local_pizza, + 'local_play': Icons.local_play, + 'local_police': Icons.local_police, + 'local_post_office': Icons.local_post_office, + 'local_printshop': Icons.local_printshop, + 'local_see': Icons.local_see, + 'local_shipping': Icons.local_shipping, + 'local_taxi': Icons.local_taxi, + 'location_city': Icons.location_city, + 'location_disabled': Icons.location_disabled, + 'location_history': Icons.location_history, + 'location_off': Icons.location_off, + 'location_on': Icons.location_on, + 'location_pin': Icons.location_pin, + 'location_searching': Icons.location_searching, + 'lock': Icons.lock, + 'lock_clock': Icons.lock_clock, + 'lock_open': Icons.lock_open, + 'lock_outline': Icons.lock_outline, + 'lock_person': Icons.lock_person, + 'lock_reset': Icons.lock_reset, + 'login': Icons.login, + 'logo_dev': Icons.logo_dev, + 'logout': Icons.logout, + 'looks': Icons.looks, + 'looks_3': Icons.looks_3, + 'looks_4': Icons.looks_4, + 'looks_5': Icons.looks_5, + 'looks_6': Icons.looks_6, + 'looks_one': Icons.looks_one, + 'looks_two': Icons.looks_two, + 'loop': Icons.loop, + 'loupe': Icons.loupe, + 'low_priority': Icons.low_priority, + 'loyalty': Icons.loyalty, + 'lte_mobiledata': Icons.lte_mobiledata, + 'lte_plus_mobiledata': Icons.lte_plus_mobiledata, + 'luggage': Icons.luggage, + 'lunch_dining': Icons.lunch_dining, + + // M + 'mail': Icons.mail, + 'mail_lock': Icons.mail_lock, + 'mail_outline': Icons.mail_outline, + 'male': Icons.male, + 'man': Icons.man, + 'manage_accounts': Icons.manage_accounts, + 'manage_history': Icons.manage_history, + 'manage_search': Icons.manage_search, + 'map': Icons.map, + 'maps_home_work': Icons.maps_home_work, + 'maps_ugc': Icons.maps_ugc, + 'margin': Icons.margin, + 'mark_as_unread': Icons.mark_as_unread, + 'mark_chat_read': Icons.mark_chat_read, + 'mark_chat_unread': Icons.mark_chat_unread, + 'mark_email_read': Icons.mark_email_read, + 'mark_email_unread': Icons.mark_email_unread, + 'mark_unread_chat_alt': Icons.mark_unread_chat_alt, + 'markunread': Icons.markunread, + 'markunread_mailbox': Icons.markunread_mailbox, + 'masks': Icons.masks, + 'maximize': Icons.maximize, + 'media_bluetooth_off': Icons.media_bluetooth_off, + 'media_bluetooth_on': Icons.media_bluetooth_on, + 'mediation': Icons.mediation, + 'medical_information': Icons.medical_information, + 'medical_services': Icons.medical_services, + 'medication': Icons.medication, + 'medication_liquid': Icons.medication_liquid, + 'meeting_room': Icons.meeting_room, + 'memory': Icons.memory, + 'menu': Icons.menu, + 'menu_book': Icons.menu_book, + 'menu_open': Icons.menu_open, + 'merge': Icons.merge, + 'merge_type': Icons.merge_type, + 'message': Icons.message, + 'messenger': Icons.messenger, + 'messenger_outline': Icons.messenger_outline, + 'mic': Icons.mic, + 'mic_external_off': Icons.mic_external_off, + 'mic_external_on': Icons.mic_external_on, + 'mic_none': Icons.mic_none, + 'mic_off': Icons.mic_off, + 'microwave': Icons.microwave, + 'military_tech': Icons.military_tech, + 'minimize': Icons.minimize, + 'minor_crash': Icons.minor_crash, + 'miscellaneous_services': Icons.miscellaneous_services, + 'missed_video_call': Icons.missed_video_call, + 'mms': Icons.mms, + 'mobile_friendly': Icons.mobile_friendly, + 'mobile_off': Icons.mobile_off, + 'mobile_screen_share': Icons.mobile_screen_share, + 'mobiledata_off': Icons.mobiledata_off, + 'mode': Icons.mode, + 'mode_comment': Icons.mode_comment, + 'mode_edit': Icons.mode_edit, + 'mode_edit_outline': Icons.mode_edit_outline, + 'mode_fan_off': Icons.mode_fan_off, + 'mode_night': Icons.mode_night, + 'mode_of_travel': Icons.mode_of_travel, + 'mode_standby': Icons.mode_standby, + 'model_training': Icons.model_training, + 'monetization_on': Icons.monetization_on, + 'money': Icons.money, + 'money_off': Icons.money_off, + 'money_off_csred': Icons.money_off_csred, + 'monitor': Icons.monitor, + 'monitor_heart': Icons.monitor_heart, + 'monitor_weight': Icons.monitor_weight, + 'monochrome_photos': Icons.monochrome_photos, + 'mood': Icons.mood, + 'mood_bad': Icons.mood_bad, + 'moped': Icons.moped, + 'more': Icons.more, + 'more_horiz': Icons.more_horiz, + 'more_time': Icons.more_time, + 'more_vert': Icons.more_vert, + 'mosque': Icons.mosque, + 'motion_photos_auto': Icons.motion_photos_auto, + 'motion_photos_off': Icons.motion_photos_off, + 'motion_photos_on': Icons.motion_photos_on, + 'motion_photos_pause': Icons.motion_photos_pause, + 'motion_photos_paused': Icons.motion_photos_paused, + 'motorcycle': Icons.motorcycle, + 'mouse': Icons.mouse, + 'move_down': Icons.move_down, + 'move_to_inbox': Icons.move_to_inbox, + 'move_up': Icons.move_up, + 'movie': Icons.movie, + 'movie_creation': Icons.movie_creation, + 'movie_filter': Icons.movie_filter, + 'moving': Icons.moving, + 'mp': Icons.mp, + 'multiline_chart': Icons.multiline_chart, + 'multiple_stop': Icons.multiple_stop, + 'museum': Icons.museum, + 'music_note': Icons.music_note, + 'music_off': Icons.music_off, + 'music_video': Icons.music_video, + 'my_library_add': Icons.my_library_add, + 'my_library_books': Icons.my_library_books, + 'my_library_music': Icons.my_library_music, + 'my_location': Icons.my_location, + + // N + 'nat': Icons.nat, + 'nature': Icons.nature, + 'nature_people': Icons.nature_people, + 'navigate_before': Icons.navigate_before, + 'navigate_next': Icons.navigate_next, + 'navigation': Icons.navigation, + 'near_me': Icons.near_me, + 'near_me_disabled': Icons.near_me_disabled, + 'nearby_error': Icons.nearby_error, + 'nearby_off': Icons.nearby_off, + 'nest_cam_wired_stand': Icons.nest_cam_wired_stand, + 'network_cell': Icons.network_cell, + 'network_check': Icons.network_check, + 'network_locked': Icons.network_locked, + 'network_ping': Icons.network_ping, + 'network_wifi': Icons.network_wifi, + 'network_wifi_1_bar': Icons.network_wifi_1_bar, + 'network_wifi_2_bar': Icons.network_wifi_2_bar, + 'network_wifi_3_bar': Icons.network_wifi_3_bar, + 'new_label': Icons.new_label, + 'new_releases': Icons.new_releases, + 'newspaper': Icons.newspaper, + 'next_plan': Icons.next_plan, + 'next_week': Icons.next_week, + 'nfc': Icons.nfc, + 'night_shelter': Icons.night_shelter, + 'nightlife': Icons.nightlife, + 'nightlight': Icons.nightlight, + 'nightlight_round': Icons.nightlight_round, + 'nights_stay': Icons.nights_stay, + 'no_accounts': Icons.no_accounts, + 'no_adult_content': Icons.no_adult_content, + 'no_backpack': Icons.no_backpack, + 'no_cell': Icons.no_cell, + 'no_crash': Icons.no_crash, + 'no_drinks': Icons.no_drinks, + 'no_encryption': Icons.no_encryption, + 'no_encryption_gmailerrorred': Icons.no_encryption_gmailerrorred, + 'no_flash': Icons.no_flash, + 'no_food': Icons.no_food, + 'no_luggage': Icons.no_luggage, + 'no_meals': Icons.no_meals, + 'no_meeting_room': Icons.no_meeting_room, + 'no_photography': Icons.no_photography, + 'no_sim': Icons.no_sim, + 'no_stroller': Icons.no_stroller, + 'no_transfer': Icons.no_transfer, + 'noise_aware': Icons.noise_aware, + 'noise_control_off': Icons.noise_control_off, + 'nordic_walking': Icons.nordic_walking, + 'north': Icons.north, + 'north_east': Icons.north_east, + 'north_west': Icons.north_west, + 'not_accessible': Icons.not_accessible, + 'not_interested': Icons.not_interested, + 'not_listed_location': Icons.not_listed_location, + 'not_started': Icons.not_started, + 'note': Icons.note, + 'note_add': Icons.note_add, + 'note_alt': Icons.note_alt, + 'notes': Icons.notes, + 'notification_add': Icons.notification_add, + 'notification_important': Icons.notification_important, + 'notifications': Icons.notifications, + 'notifications_active': Icons.notifications_active, + 'notifications_none': Icons.notifications_none, + 'notifications_off': Icons.notifications_off, + 'notifications_on': Icons.notifications_on, + 'notifications_outlined': Icons.notifications_outlined, + 'notifications_paused': Icons.notifications_paused, + 'now_wallpaper': Icons.now_wallpaper, + 'now_widgets': Icons.now_widgets, + 'numbers': Icons.numbers, + + // O + 'offline_bolt': Icons.offline_bolt, + 'offline_pin': Icons.offline_pin, + 'offline_share': Icons.offline_share, + 'oil_barrel': Icons.oil_barrel, + 'on_device_training': Icons.on_device_training, + 'ondemand_video': Icons.ondemand_video, + 'one_k': Icons.one_k, + 'one_k_plus': Icons.one_k_plus, + 'one_x_mobiledata': Icons.one_x_mobiledata, + 'online_prediction': Icons.online_prediction, + 'opacity': Icons.opacity, + 'open_in_browser': Icons.open_in_browser, + 'open_in_full': Icons.open_in_full, + 'open_in_new': Icons.open_in_new, + 'open_in_new_off': Icons.open_in_new_off, + 'open_with': Icons.open_with, + 'other_houses': Icons.other_houses, + 'outbound': Icons.outbound, + 'outbox': Icons.outbox, + 'outdoor_grill': Icons.outdoor_grill, + 'outgoing_mail': Icons.outgoing_mail, + 'outlet': Icons.outlet, + 'outlined_flag': Icons.outlined_flag, + 'output': Icons.output, + 'padding': Icons.padding, + + // P + 'pages': Icons.pages, + 'pageview': Icons.pageview, + 'paid': Icons.paid, + 'palette': Icons.palette, + 'pan_tool': Icons.pan_tool, + 'pan_tool_alt': Icons.pan_tool_alt, + 'panorama': Icons.panorama, + 'panorama_fish_eye': Icons.panorama_fish_eye, + 'panorama_horizontal': Icons.panorama_horizontal, + 'panorama_horizontal_select': Icons.panorama_horizontal_select, + 'panorama_photosphere': Icons.panorama_photosphere, + 'panorama_photosphere_select': Icons.panorama_photosphere_select, + 'panorama_vertical': Icons.panorama_vertical, + 'panorama_vertical_select': Icons.panorama_vertical_select, + 'panorama_wide_angle': Icons.panorama_wide_angle, + 'panorama_wide_angle_select': Icons.panorama_wide_angle_select, + 'paragliding': Icons.paragliding, + 'park': Icons.park, + 'party_mode': Icons.party_mode, + 'password': Icons.password, + 'paste': Icons.paste, + 'pattern': Icons.pattern, + 'pause': Icons.pause, + 'pause_circle': Icons.pause_circle, + 'pause_circle_filled': Icons.pause_circle_filled, + 'pause_circle_outline': Icons.pause_circle_outline, + 'pause_presentation': Icons.pause_presentation, + 'payment': Icons.payment, + 'payments': Icons.payments, + 'paypal': Icons.paypal, + 'pedal_bike': Icons.pedal_bike, + 'pending': Icons.pending, + 'pending_actions': Icons.pending_actions, + 'pentagon': Icons.pentagon, + 'people': Icons.people, + 'people_alt': Icons.people_alt, + 'people_outline': Icons.people_outline, + 'percent': Icons.percent, + 'perm_camera_mic': Icons.perm_camera_mic, + 'perm_contact_calendar': Icons.perm_contact_calendar, + 'perm_data_setting': Icons.perm_data_setting, + 'perm_device_information': Icons.perm_device_information, + 'perm_identity': Icons.perm_identity, + 'perm_media': Icons.perm_media, + 'perm_phone_msg': Icons.perm_phone_msg, + 'perm_scan_wifi': Icons.perm_scan_wifi, + 'person': Icons.person, + 'person_2': Icons.person_2, + 'person_3': Icons.person_3, + 'person_4': Icons.person_4, + 'person_add': Icons.person_add, + 'person_add_alt': Icons.person_add_alt, + 'person_add_alt_1': Icons.person_add_alt_1, + 'person_add_disabled': Icons.person_add_disabled, + 'person_off': Icons.person_off, + 'person_outline': Icons.person_outline, + 'person_pin': Icons.person_pin, + 'person_pin_circle': Icons.person_pin_circle, + 'person_remove': Icons.person_remove, + 'person_remove_alt_1': Icons.person_remove_alt_1, + 'person_search': Icons.person_search, + 'personal_injury': Icons.personal_injury, + 'personal_video': Icons.personal_video, + 'pest_control': Icons.pest_control, + 'pest_control_rodent': Icons.pest_control_rodent, + 'pets': Icons.pets, + 'phishing': Icons.phishing, + 'phone': Icons.phone, + 'phone_android': Icons.phone_android, + 'phone_bluetooth_speaker': Icons.phone_bluetooth_speaker, + 'phone_callback': Icons.phone_callback, + 'phone_disabled': Icons.phone_disabled, + 'phone_enabled': Icons.phone_enabled, + 'phone_forwarded': Icons.phone_forwarded, + 'phone_in_talk': Icons.phone_in_talk, + 'phone_iphone': Icons.phone_iphone, + 'phone_locked': Icons.phone_locked, + 'phone_missed': Icons.phone_missed, + 'phone_paused': Icons.phone_paused, + 'phonelink': Icons.phonelink, + 'phonelink_erase': Icons.phonelink_erase, + 'phonelink_lock': Icons.phonelink_lock, + 'phonelink_off': Icons.phonelink_off, + 'phonelink_ring': Icons.phonelink_ring, + 'phonelink_setup': Icons.phonelink_setup, + 'photo': Icons.photo, + 'photo_album': Icons.photo_album, + 'photo_camera': Icons.photo_camera, + 'photo_camera_back': Icons.photo_camera_back, + 'photo_camera_front': Icons.photo_camera_front, + 'photo_filter': Icons.photo_filter, + 'photo_library': Icons.photo_library, + 'photo_size_select_actual': Icons.photo_size_select_actual, + 'photo_size_select_large': Icons.photo_size_select_large, + 'photo_size_select_small': Icons.photo_size_select_small, + 'php': Icons.php, + 'piano': Icons.piano, + 'piano_off': Icons.piano_off, + 'picture_as_pdf': Icons.picture_as_pdf, + 'picture_in_picture': Icons.picture_in_picture, + 'picture_in_picture_alt': Icons.picture_in_picture_alt, + 'pie_chart': Icons.pie_chart, + 'pie_chart_outline': Icons.pie_chart_outline, + 'pin': Icons.pin, + 'pin_drop': Icons.pin_drop, + 'pin_end': Icons.pin_end, + 'pin_invoke': Icons.pin_invoke, + 'pinch': Icons.pinch, + 'pivot_table_chart': Icons.pivot_table_chart, + 'pix': Icons.pix, + 'place': Icons.place, + 'plagiarism': Icons.plagiarism, + 'play_arrow': Icons.play_arrow, + 'play_circle': Icons.play_circle, + 'play_circle_fill': Icons.play_circle_fill, + 'play_circle_filled': Icons.play_circle_filled, + 'play_circle_outline': Icons.play_circle_outline, + 'play_disabled': Icons.play_disabled, + 'play_for_work': Icons.play_for_work, + 'play_lesson': Icons.play_lesson, + 'playlist_add': Icons.playlist_add, + 'playlist_add_check': Icons.playlist_add_check, + 'playlist_add_check_circle': Icons.playlist_add_check_circle, + 'playlist_add_circle': Icons.playlist_add_circle, + 'playlist_play': Icons.playlist_play, + 'playlist_remove': Icons.playlist_remove, + 'plumbing': Icons.plumbing, + 'plus_one': Icons.plus_one, + 'podcasts': Icons.podcasts, + 'point_of_sale': Icons.point_of_sale, + 'policy': Icons.policy, + 'poll': Icons.poll, + 'polyline': Icons.polyline, + 'polymer': Icons.polymer, + 'pool': Icons.pool, + 'portable_wifi_off': Icons.portable_wifi_off, + 'portrait': Icons.portrait, + 'post_add': Icons.post_add, + 'power': Icons.power, + 'power_input': Icons.power_input, + 'power_off': Icons.power_off, + 'power_settings_new': Icons.power_settings_new, + 'precision_manufacturing': Icons.precision_manufacturing, + 'pregnant_woman': Icons.pregnant_woman, + 'present_to_all': Icons.present_to_all, + 'preview': Icons.preview, + 'price_change': Icons.price_change, + 'price_check': Icons.price_check, + 'print': Icons.print, + 'print_disabled': Icons.print_disabled, + 'priority_high': Icons.priority_high, + 'privacy_tip': Icons.privacy_tip, + 'privacy_tip_outlined': Icons.privacy_tip_outlined, + 'private_connectivity': Icons.private_connectivity, + 'production_quantity_limits': Icons.production_quantity_limits, + 'propane': Icons.propane, + 'propane_tank': Icons.propane_tank, + 'psychology': Icons.psychology, + 'psychology_alt': Icons.psychology_alt, + 'public': Icons.public, + 'public_off': Icons.public_off, + 'publish': Icons.publish, + 'published_with_changes': Icons.published_with_changes, + 'punch_clock': Icons.punch_clock, + 'push_pin': Icons.push_pin, + 'qr_code': Icons.qr_code, + + // Q + 'qr_code_2': Icons.qr_code_2, + 'qr_code_scanner': Icons.qr_code_scanner, + 'query_builder': Icons.query_builder, + 'query_stats': Icons.query_stats, + 'question_answer': Icons.question_answer, + 'question_mark': Icons.question_mark, + 'queue': Icons.queue, + 'queue_music': Icons.queue_music, + 'queue_play_next': Icons.queue_play_next, + 'quickreply': Icons.quickreply, + 'quiz': Icons.quiz, + + // R + 'r_mobiledata': Icons.r_mobiledata, + 'radar': Icons.radar, + 'radio': Icons.radio, + 'radio_button_checked': Icons.radio_button_checked, + 'radio_button_off': Icons.radio_button_off, + 'radio_button_on': Icons.radio_button_on, + 'radio_button_unchecked': Icons.radio_button_unchecked, + 'railway_alert': Icons.railway_alert, + 'ramen_dining': Icons.ramen_dining, + 'ramp_left': Icons.ramp_left, + 'ramp_right': Icons.ramp_right, + 'rate_review': Icons.rate_review, + 'raw_off': Icons.raw_off, + 'raw_on': Icons.raw_on, + 'read_more': Icons.read_more, + 'real_estate_agent': Icons.real_estate_agent, + 'receipt': Icons.receipt, + 'receipt_long': Icons.receipt_long, + 'recent_actors': Icons.recent_actors, + 'recommend': Icons.recommend, + 'record_voice_over': Icons.record_voice_over, + 'rectangle': Icons.rectangle, + 'recycling': Icons.recycling, + 'redeem': Icons.redeem, + 'redo': Icons.redo, + 'reduce_capacity': Icons.reduce_capacity, + 'refresh': Icons.refresh, + 'remember_me': Icons.remember_me, + 'remove': Icons.remove, + 'remove_circle': Icons.remove_circle, + 'remove_circle_outline': Icons.remove_circle_outline, + 'remove_done': Icons.remove_done, + 'remove_from_queue': Icons.remove_from_queue, + 'remove_moderator': Icons.remove_moderator, + 'remove_red_eye': Icons.remove_red_eye, + 'remove_red_eye_outlined': Icons.remove_red_eye_outlined, + 'remove_shopping_cart': Icons.remove_shopping_cart, + 'reorder': Icons.reorder, + 'repartition': Icons.repartition, + 'repeat': Icons.repeat, + 'repeat_on': Icons.repeat_on, + 'repeat_one': Icons.repeat_one, + 'repeat_one_on': Icons.repeat_one_on, + 'replay': Icons.replay, + 'replay_10': Icons.replay_10, + 'replay_30': Icons.replay_30, + 'replay_5': Icons.replay_5, + 'replay_circle_filled': Icons.replay_circle_filled, + 'reply': Icons.reply, + 'reply_all': Icons.reply_all, + 'report': Icons.report, + 'report_gmailerrorred': Icons.report_gmailerrorred, + 'report_off': Icons.report_off, + 'report_problem': Icons.report_problem, + 'request_page': Icons.request_page, + 'request_quote': Icons.request_quote, + 'reset_tv': Icons.reset_tv, + 'restart_alt': Icons.restart_alt, + 'restaurant': Icons.restaurant, + 'restaurant_menu': Icons.restaurant_menu, + 'restore': Icons.restore, + 'restore_from_trash': Icons.restore_from_trash, + 'restore_page': Icons.restore_page, + 'reviews': Icons.reviews, + 'rice_bowl': Icons.rice_bowl, + 'ring_volume': Icons.ring_volume, + 'rocket': Icons.rocket, + 'rocket_launch': Icons.rocket_launch, + 'roller_shades': Icons.roller_shades, + 'roller_shades_closed': Icons.roller_shades_closed, + 'roller_skating': Icons.roller_skating, + 'roofing': Icons.roofing, + 'room': Icons.room, + 'room_preferences': Icons.room_preferences, + 'room_service': Icons.room_service, + 'rotate_90_degrees_ccw': Icons.rotate_90_degrees_ccw, + 'rotate_90_degrees_cw': Icons.rotate_90_degrees_cw, + 'rotate_left': Icons.rotate_left, + 'rotate_right': Icons.rotate_right, + 'roundabout_left': Icons.roundabout_left, + 'roundabout_right': Icons.roundabout_right, + 'rounded_corner': Icons.rounded_corner, + 'route': Icons.route, + 'router': Icons.router, + 'rowing': Icons.rowing, + 'rss_feed': Icons.rss_feed, + 'rsvp': Icons.rsvp, + 'rtt': Icons.rtt, + 'rule': Icons.rule, + 'rule_folder': Icons.rule_folder, + 'run_circle': Icons.run_circle, + 'running_with_errors': Icons.running_with_errors, + 'rv_hookup': Icons.rv_hookup, + + // S + 'safety_check': Icons.safety_check, + 'safety_divider': Icons.safety_divider, + 'sailing': Icons.sailing, + 'sanitizer': Icons.sanitizer, + 'satellite': Icons.satellite, + 'satellite_alt': Icons.satellite_alt, + 'save': Icons.save, + 'save_alt': Icons.save_alt, + 'save_as': Icons.save_as, + 'saved_search': Icons.saved_search, + 'savings': Icons.savings, + 'scale': Icons.scale, + 'scanner': Icons.scanner, + 'scatter_plot': Icons.scatter_plot, + 'schedule': Icons.schedule, + 'schedule_send': Icons.schedule_send, + 'schema': Icons.schema, + 'school': Icons.school, + 'science': Icons.science, + 'score': Icons.score, + 'scoreboard': Icons.scoreboard, + 'screen_lock_landscape': Icons.screen_lock_landscape, + 'screen_lock_portrait': Icons.screen_lock_portrait, + 'screen_lock_rotation': Icons.screen_lock_rotation, + 'screen_rotation': Icons.screen_rotation, + 'screen_rotation_alt': Icons.screen_rotation_alt, + 'screen_search_desktop': Icons.screen_search_desktop, + 'screen_share': Icons.screen_share, + 'screenshot': Icons.screenshot, + 'screenshot_monitor': Icons.screenshot_monitor, + 'scuba_diving': Icons.scuba_diving, + 'sd': Icons.sd, + 'sd_card': Icons.sd_card, + 'sd_card_alert': Icons.sd_card_alert, + 'sd_storage': Icons.sd_storage, + 'search': Icons.search, + 'search_off': Icons.search_off, + 'security': Icons.security, + 'security_update': Icons.security_update, + 'security_update_good': Icons.security_update_good, + 'security_update_warning': Icons.security_update_warning, + 'segment': Icons.segment, + 'select_all': Icons.select_all, + 'self_improvement': Icons.self_improvement, + 'sell': Icons.sell, + 'send': Icons.send, + 'send_and_archive': Icons.send_and_archive, + 'send_time_extension': Icons.send_time_extension, + 'send_to_mobile': Icons.send_to_mobile, + 'sensor_door': Icons.sensor_door, + 'sensor_occupied': Icons.sensor_occupied, + 'sensor_window': Icons.sensor_window, + 'sensors': Icons.sensors, + 'sensors_off': Icons.sensors_off, + 'sentiment_dissatisfied': Icons.sentiment_dissatisfied, + 'sentiment_neutral': Icons.sentiment_neutral, + 'sentiment_satisfied': Icons.sentiment_satisfied, + 'sentiment_satisfied_alt': Icons.sentiment_satisfied_alt, + 'sentiment_very_dissatisfied': Icons.sentiment_very_dissatisfied, + 'sentiment_very_satisfied': Icons.sentiment_very_satisfied, + 'set_meal': Icons.set_meal, + 'settings': Icons.settings, + 'settings_accessibility': Icons.settings_accessibility, + 'settings_applications': Icons.settings_applications, + 'settings_backup_restore': Icons.settings_backup_restore, + 'settings_bluetooth': Icons.settings_bluetooth, + 'settings_brightness': Icons.settings_brightness, + 'settings_cell': Icons.settings_cell, + 'settings_display': Icons.settings_display, + 'settings_ethernet': Icons.settings_ethernet, + 'settings_input_antenna': Icons.settings_input_antenna, + 'settings_input_component': Icons.settings_input_component, + 'settings_input_composite': Icons.settings_input_composite, + 'settings_input_hdmi': Icons.settings_input_hdmi, + 'settings_input_svideo': Icons.settings_input_svideo, + 'settings_overscan': Icons.settings_overscan, + 'settings_phone': Icons.settings_phone, + 'settings_power': Icons.settings_power, + 'settings_remote': Icons.settings_remote, + 'settings_suggest': Icons.settings_suggest, + 'settings_system_daydream': Icons.settings_system_daydream, + 'settings_voice': Icons.settings_voice, + 'severe_cold': Icons.severe_cold, + 'shape_line': Icons.shape_line, + 'share': Icons.share, + 'share_location': Icons.share_location, + 'shield': Icons.shield, + 'shield_moon': Icons.shield_moon, + 'shop': Icons.shop, + 'shop_2': Icons.shop_2, + 'shop_two': Icons.shop_two, + 'shopping_bag': Icons.shopping_bag, + 'shopping_basket': Icons.shopping_basket, + 'shopping_cart': Icons.shopping_cart, + 'shopping_cart_checkout': Icons.shopping_cart_checkout, + 'short_text': Icons.short_text, + 'shortcut': Icons.shortcut, + 'show_chart': Icons.show_chart, + 'shower': Icons.shower, + 'shuffle': Icons.shuffle, + 'shuffle_on': Icons.shuffle_on, + 'shutter_speed': Icons.shutter_speed, + 'sick': Icons.sick, + 'sign_language': Icons.sign_language, + 'signal_cellular_0_bar': Icons.signal_cellular_0_bar, + 'signal_cellular_4_bar': Icons.signal_cellular_4_bar, + 'signal_cellular_alt': Icons.signal_cellular_alt, + 'signal_cellular_alt_1_bar': Icons.signal_cellular_alt_1_bar, + 'signal_cellular_alt_2_bar': Icons.signal_cellular_alt_2_bar, + 'signal_cellular_connected_no_internet_0_bar': + Icons.signal_cellular_connected_no_internet_0_bar, + 'signal_cellular_connected_no_internet_4_bar': + Icons.signal_cellular_connected_no_internet_4_bar, + 'signal_cellular_no_sim': Icons.signal_cellular_no_sim, + 'signal_cellular_nodata': Icons.signal_cellular_nodata, + 'signal_cellular_null': Icons.signal_cellular_null, + 'signal_cellular_off': Icons.signal_cellular_off, + 'signal_wifi_0_bar': Icons.signal_wifi_0_bar, + 'signal_wifi_4_bar': Icons.signal_wifi_4_bar, + 'signal_wifi_4_bar_lock': Icons.signal_wifi_4_bar_lock, + 'signal_wifi_bad': Icons.signal_wifi_bad, + 'signal_wifi_connected_no_internet_4': + Icons.signal_wifi_connected_no_internet_4, + 'signal_wifi_off': Icons.signal_wifi_off, + 'signal_wifi_statusbar_4_bar': Icons.signal_wifi_statusbar_4_bar, + 'signal_wifi_statusbar_connected_no_internet_4': + Icons.signal_wifi_statusbar_connected_no_internet_4, + 'signal_wifi_statusbar_null': Icons.signal_wifi_statusbar_null, + 'signpost': Icons.signpost, + 'sim_card': Icons.sim_card, + 'sim_card_alert': Icons.sim_card_alert, + 'sim_card_download': Icons.sim_card_download, + 'single_bed': Icons.single_bed, + 'sip': Icons.sip, + 'skateboarding': Icons.skateboarding, + 'skip_next': Icons.skip_next, + 'skip_previous': Icons.skip_previous, + 'sledding': Icons.sledding, + 'slideshow': Icons.slideshow, + 'slow_motion_video': Icons.slow_motion_video, + 'smart_button': Icons.smart_button, + 'smart_display': Icons.smart_display, + 'smart_screen': Icons.smart_screen, + 'smart_toy': Icons.smart_toy, + 'smartphone': Icons.smartphone, + 'smoke_free': Icons.smoke_free, + 'smoking_rooms': Icons.smoking_rooms, + 'sms': Icons.sms, + 'sms_failed': Icons.sms_failed, + 'snippet_folder': Icons.snippet_folder, + 'snooze': Icons.snooze, + 'snowboarding': Icons.snowboarding, + 'snowmobile': Icons.snowmobile, + 'snowshoeing': Icons.snowshoeing, + 'soap': Icons.soap, + 'social_distance': Icons.social_distance, + 'solar_power': Icons.solar_power, + 'sort': Icons.sort, + 'sort_by_alpha': Icons.sort_by_alpha, + 'sos': Icons.sos, + 'soup_kitchen': Icons.soup_kitchen, + 'source': Icons.source, + 'south': Icons.south, + 'south_america': Icons.south_america, + 'south_east': Icons.south_east, + 'south_west': Icons.south_west, + 'spa': Icons.spa, + 'space_bar': Icons.space_bar, + 'space_dashboard': Icons.space_dashboard, + 'spatial_audio': Icons.spatial_audio, + 'spatial_audio_off': Icons.spatial_audio_off, + 'spatial_tracking': Icons.spatial_tracking, + 'speaker': Icons.speaker, + 'speaker_group': Icons.speaker_group, + 'speaker_notes': Icons.speaker_notes, + 'speaker_notes_off': Icons.speaker_notes_off, + 'speaker_phone': Icons.speaker_phone, + 'speed': Icons.speed, + 'spellcheck': Icons.spellcheck, + 'splitscreen': Icons.splitscreen, + 'spoke': Icons.spoke, + 'sports': Icons.sports, + 'sports_bar': Icons.sports_bar, + 'sports_baseball': Icons.sports_baseball, + 'sports_basketball': Icons.sports_basketball, + 'sports_cricket': Icons.sports_cricket, + 'sports_esports': Icons.sports_esports, + 'sports_football': Icons.sports_football, + 'sports_golf': Icons.sports_golf, + 'sports_gymnastics': Icons.sports_gymnastics, + 'sports_handball': Icons.sports_handball, + 'sports_hockey': Icons.sports_hockey, + 'sports_kabaddi': Icons.sports_kabaddi, + 'sports_martial_arts': Icons.sports_martial_arts, + 'sports_mma': Icons.sports_mma, + 'sports_motorsports': Icons.sports_motorsports, + 'sports_rugby': Icons.sports_rugby, + 'sports_score': Icons.sports_score, + 'sports_soccer': Icons.sports_soccer, + 'sports_tennis': Icons.sports_tennis, + 'sports_volleyball': Icons.sports_volleyball, + 'square': Icons.square, + 'square_foot': Icons.square_foot, + 'ssid_chart': Icons.ssid_chart, + 'stacked_bar_chart': Icons.stacked_bar_chart, + 'stacked_line_chart': Icons.stacked_line_chart, + 'stadium': Icons.stadium, + 'stairs': Icons.stairs, + 'star': Icons.star, + 'star_border': Icons.star_border, + 'star_border_purple500': Icons.star_border_purple500, + 'star_half': Icons.star_half, + 'star_outline': Icons.star_outline, + 'star_purple500': Icons.star_purple500, + 'star_rate': Icons.star_rate, + 'stars': Icons.stars, + 'start': Icons.start, + 'stay_current_landscape': Icons.stay_current_landscape, + 'stay_current_portrait': Icons.stay_current_portrait, + 'stay_primary_landscape': Icons.stay_primary_landscape, + 'stay_primary_portrait': Icons.stay_primary_portrait, + 'sticky_note_2': Icons.sticky_note_2, + 'stop': Icons.stop, + 'stop_circle': Icons.stop_circle, + 'stop_screen_share': Icons.stop_screen_share, + 'storage': Icons.storage, + 'store': Icons.store, + 'store_mall_directory': Icons.store_mall_directory, + 'storefront': Icons.storefront, + 'storm': Icons.storm, + 'straight': Icons.straight, + 'straighten': Icons.straighten, + 'stream': Icons.stream, + 'streetview': Icons.streetview, + 'strikethrough_s': Icons.strikethrough_s, + 'stroller': Icons.stroller, + 'style': Icons.style, + 'subdirectory_arrow_left': Icons.subdirectory_arrow_left, + 'subdirectory_arrow_right': Icons.subdirectory_arrow_right, + 'subject': Icons.subject, + 'subscript': Icons.subscript, + 'subscriptions': Icons.subscriptions, + 'subtitles': Icons.subtitles, + 'subtitles_off': Icons.subtitles_off, + 'subway': Icons.subway, + 'summarize': Icons.summarize, + 'superscript': Icons.superscript, + 'supervised_user_circle': Icons.supervised_user_circle, + 'supervisor_account': Icons.supervisor_account, + 'support': Icons.support, + 'support_agent': Icons.support_agent, + 'surfing': Icons.surfing, + 'surround_sound': Icons.surround_sound, + 'swap_calls': Icons.swap_calls, + 'swap_horiz': Icons.swap_horiz, + 'swap_horizontal_circle': Icons.swap_horizontal_circle, + 'swap_vert': Icons.swap_vert, + 'swap_vertical_circle': Icons.swap_vertical_circle, + 'swipe': Icons.swipe, + 'swipe_down': Icons.swipe_down, + 'swipe_down_alt': Icons.swipe_down_alt, + 'swipe_left': Icons.swipe_left, + 'swipe_left_alt': Icons.swipe_left_alt, + 'swipe_right': Icons.swipe_right, + 'swipe_right_alt': Icons.swipe_right_alt, + 'swipe_up': Icons.swipe_up, + 'swipe_up_alt': Icons.swipe_up_alt, + 'swipe_vertical': Icons.swipe_vertical, + 'switch_access_shortcut': Icons.switch_access_shortcut, + 'switch_access_shortcut_add': Icons.switch_access_shortcut_add, + 'switch_account': Icons.switch_account, + 'switch_camera': Icons.switch_camera, + 'switch_left': Icons.switch_left, + 'switch_right': Icons.switch_right, + 'switch_video': Icons.switch_video, + 'synagogue': Icons.synagogue, + 'sync': Icons.sync, + 'sync_alt': Icons.sync_alt, + 'sync_disabled': Icons.sync_disabled, + 'sync_problem': Icons.sync_problem, + 'system_security_update': Icons.system_security_update, + 'system_security_update_good': Icons.system_security_update_good, + 'system_security_update_warning': Icons.system_security_update_warning, + 'system_update': Icons.system_update, + 'system_update_alt': Icons.system_update_alt, + + // T + 'tab': Icons.tab, + 'tab_unselected': Icons.tab_unselected, + 'table_bar': Icons.table_bar, + 'table_chart': Icons.table_chart, + 'table_restaurant': Icons.table_restaurant, + 'table_rows': Icons.table_rows, + 'table_view': Icons.table_view, + 'tablet': Icons.tablet, + 'tablet_android': Icons.tablet_android, + 'tablet_mac': Icons.tablet_mac, + 'tag': Icons.tag, + 'tag_faces': Icons.tag_faces, + 'takeout_dining': Icons.takeout_dining, + 'tap_and_play': Icons.tap_and_play, + 'tapas': Icons.tapas, + 'task': Icons.task, + 'task_alt': Icons.task_alt, + 'taxi_alert': Icons.taxi_alert, + 'telegram': Icons.telegram, + 'temple_buddhist': Icons.temple_buddhist, + 'temple_hindu': Icons.temple_hindu, + 'terminal': Icons.terminal, + 'terrain': Icons.terrain, + 'text_decrease': Icons.text_decrease, + 'text_fields': Icons.text_fields, + 'text_format': Icons.text_format, + 'text_increase': Icons.text_increase, + 'text_rotate_up': Icons.text_rotate_up, + 'text_rotate_vertical': Icons.text_rotate_vertical, + 'text_rotation_angledown': Icons.text_rotation_angledown, + 'text_rotation_angleup': Icons.text_rotation_angleup, + 'text_rotation_down': Icons.text_rotation_down, + 'text_rotation_none': Icons.text_rotation_none, + 'text_snippet': Icons.text_snippet, + 'textsms': Icons.textsms, + 'texture': Icons.texture, + 'theater_comedy': Icons.theater_comedy, + 'theaters': Icons.theaters, + 'thermostat': Icons.thermostat, + 'thermostat_auto': Icons.thermostat_auto, + 'thumb_down': Icons.thumb_down, + 'thumb_down_alt': Icons.thumb_down_alt, + 'thumb_down_off_alt': Icons.thumb_down_off_alt, + 'thumb_up': Icons.thumb_up, + 'thumb_up_alt': Icons.thumb_up_alt, + 'thumb_up_off_alt': Icons.thumb_up_off_alt, + 'thumbs_up_down': Icons.thumbs_up_down, + 'thunderstorm': Icons.thunderstorm, + 'time_to_leave': Icons.time_to_leave, + 'timelapse': Icons.timelapse, + 'timeline': Icons.timeline, + 'timer': Icons.timer, + 'timer_10': Icons.timer_10, + 'timer_10_select': Icons.timer_10_select, + 'timer_3': Icons.timer_3, + 'timer_3_select': Icons.timer_3_select, + 'timer_off': Icons.timer_off, + 'tips_and_updates': Icons.tips_and_updates, + 'tire_repair': Icons.tire_repair, + 'title': Icons.title, + 'toc': Icons.toc, + 'today': Icons.today, + 'toggle_off': Icons.toggle_off, + 'toggle_on': Icons.toggle_on, + 'token': Icons.token, + 'toll': Icons.toll, + 'tonality': Icons.tonality, + 'topic': Icons.topic, + 'tornado': Icons.tornado, + 'touch_app': Icons.touch_app, + 'tour': Icons.tour, + 'toys': Icons.toys, + 'track_changes': Icons.track_changes, + 'traffic': Icons.traffic, + 'train': Icons.train, + 'tram': Icons.tram, + 'transcribe': Icons.transcribe, + 'transfer_within_a_station': Icons.transfer_within_a_station, + 'transform': Icons.transform, + 'transgender': Icons.transgender, + 'transit_enterexit': Icons.transit_enterexit, + 'translate': Icons.translate, + 'travel_explore': Icons.travel_explore, + 'trending_down': Icons.trending_down, + 'trending_flat': Icons.trending_flat, + 'trending_up': Icons.trending_up, + 'trip_origin': Icons.trip_origin, + 'troubleshoot': Icons.troubleshoot, + 'try': Icons.try_sms_star, + 'tsunami': Icons.tsunami, + 'tty': Icons.tty, + 'tune': Icons.tune, + 'tungsten': Icons.tungsten, + 'turn_left': Icons.turn_left, + 'turn_right': Icons.turn_right, + 'turn_sharp_left': Icons.turn_sharp_left, + 'turn_sharp_right': Icons.turn_sharp_right, + 'turn_slight_left': Icons.turn_slight_left, + 'turn_slight_right': Icons.turn_slight_right, + 'turned_in': Icons.turned_in, + 'turned_in_not': Icons.turned_in_not, + 'tv': Icons.tv, + 'tv_off': Icons.tv_off, + 'two_wheeler': Icons.two_wheeler, + 'type_specimen': Icons.type_specimen, + + // U + 'u_turn_left': Icons.u_turn_left, + 'u_turn_right': Icons.u_turn_right, + 'umbrella': Icons.umbrella, + 'unarchive': Icons.unarchive, + 'undo': Icons.undo, + 'unfold_less': Icons.unfold_less, + 'unfold_more': Icons.unfold_more, + 'unpublished': Icons.unpublished, + 'unsubscribe': Icons.unsubscribe, + 'upcoming': Icons.upcoming, + 'update': Icons.update, + 'update_disabled': Icons.update_disabled, + 'upgrade': Icons.upgrade, + 'upload': Icons.upload, + 'upload_file': Icons.upload_file, + 'usb': Icons.usb, + 'usb_off': Icons.usb_off, + + // V + 'verified': Icons.verified, + 'verified_user': Icons.verified_user, + 'vertical_align_bottom': Icons.vertical_align_bottom, + 'vertical_align_center': Icons.vertical_align_center, + 'vertical_align_top': Icons.vertical_align_top, + 'vertical_distribute': Icons.vertical_distribute, + 'vertical_split': Icons.vertical_split, + 'vibration': Icons.vibration, + 'video_call': Icons.video_call, + 'video_camera_back': Icons.video_camera_back, + 'video_camera_front': Icons.video_camera_front, + 'video_chat': Icons.video_chat, + 'video_collection': Icons.video_collection, + 'video_file': Icons.video_file, + 'video_label': Icons.video_label, + 'video_library': Icons.video_library, + 'video_settings': Icons.video_settings, + 'video_stable': Icons.video_stable, + 'videocam': Icons.videocam, + 'videocam_off': Icons.videocam_off, + 'videogame_asset': Icons.videogame_asset, + 'videogame_asset_off': Icons.videogame_asset_off, + 'view_agenda': Icons.view_agenda, + 'view_array': Icons.view_array, + 'view_carousel': Icons.view_carousel, + 'view_column': Icons.view_column, + 'view_comfortable': Icons.view_comfortable, + 'view_comfy': Icons.view_comfy, + 'view_comfy_alt': Icons.view_comfy_alt, + 'view_compact': Icons.view_compact, + 'view_compact_alt': Icons.view_compact_alt, + 'view_cozy': Icons.view_cozy, + 'view_day': Icons.view_day, + 'view_headline': Icons.view_headline, + 'view_in_ar': Icons.view_in_ar, + 'view_kanban': Icons.view_kanban, + 'view_list': Icons.view_list, + 'view_module': Icons.view_module, + 'view_quilt': Icons.view_quilt, + 'view_sidebar': Icons.view_sidebar, + 'view_stream': Icons.view_stream, + 'view_timeline': Icons.view_timeline, + 'view_week': Icons.view_week, + 'vignette': Icons.vignette, + 'villa': Icons.villa, + 'visibility': Icons.visibility, + 'visibility_off': Icons.visibility_off, + 'voice_chat': Icons.voice_chat, + 'voice_over_off': Icons.voice_over_off, + 'voicemail': Icons.voicemail, + 'volcano': Icons.volcano, + 'volume_down': Icons.volume_down, + 'volume_down_alt': Icons.volume_down_alt, + 'volume_mute': Icons.volume_mute, + 'volume_off': Icons.volume_off, + 'volume_up': Icons.volume_up, + 'volunteer_activism': Icons.volunteer_activism, + 'vpn_key': Icons.vpn_key, + 'vpn_key_off': Icons.vpn_key_off, + 'vpn_lock': Icons.vpn_lock, + + // W + 'wallet': Icons.wallet, + 'wallet_giftcard': Icons.wallet_giftcard, + 'wallet_membership': Icons.wallet_membership, + 'wallet_travel': Icons.wallet_travel, + 'wallpaper': Icons.wallpaper, + 'warehouse': Icons.warehouse, + 'warning': Icons.warning, + 'warning_amber': Icons.warning_amber, + 'wash': Icons.wash, + 'watch': Icons.watch, + 'watch_later': Icons.watch_later, + 'watch_off': Icons.watch_off, + 'water': Icons.water, + 'water_damage': Icons.water_damage, + 'water_drop': Icons.water_drop, + 'waterfall_chart': Icons.waterfall_chart, + 'waves': Icons.waves, + 'waving_hand': Icons.waving_hand, + 'wb_auto': Icons.wb_auto, + 'wb_cloudy': Icons.wb_cloudy, + 'wb_incandescent': Icons.wb_incandescent, + 'wb_iridescent': Icons.wb_iridescent, + 'wb_shade': Icons.wb_shade, + 'wb_sunny': Icons.wb_sunny, + 'wb_twilight': Icons.wb_twilight, + 'wc': Icons.wc, + 'web': Icons.web, + 'web_asset': Icons.web_asset, + 'web_asset_off': Icons.web_asset_off, + 'web_stories': Icons.web_stories, + 'webhook': Icons.webhook, + 'weekend': Icons.weekend, + 'west': Icons.west, + 'whatshot': Icons.whatshot, + 'wheelchair_pickup': Icons.wheelchair_pickup, + 'where_to_vote': Icons.where_to_vote, + 'widgets': Icons.widgets, + 'width_full': Icons.width_full, + 'width_normal': Icons.width_normal, + 'width_wide': Icons.width_wide, + 'wifi': Icons.wifi, + 'wifi_1_bar': Icons.wifi_1_bar, + 'wifi_2_bar': Icons.wifi_2_bar, + 'wifi_calling': Icons.wifi_calling, + 'wifi_calling_3': Icons.wifi_calling_3, + 'wifi_channel': Icons.wifi_channel, + 'wifi_find': Icons.wifi_find, + 'wifi_lock': Icons.wifi_lock, + 'wifi_off': Icons.wifi_off, + 'wifi_password': Icons.wifi_password, + 'wifi_protected_setup': Icons.wifi_protected_setup, + 'wifi_tethering': Icons.wifi_tethering, + 'wifi_tethering_error': Icons.wifi_tethering_error, + 'wifi_tethering_off': Icons.wifi_tethering_off, + 'window': Icons.window, + 'wine_bar': Icons.wine_bar, + 'woman': Icons.woman, + 'woman_2': Icons.woman_2, + 'work': Icons.work, + 'work_history': Icons.work_history, + 'work_off': Icons.work_off, + 'work_outline': Icons.work_outline, + 'workspace_premium': Icons.workspace_premium, + 'workspaces': Icons.workspaces, + 'workspaces_filled': Icons.workspaces_filled, + 'workspaces_outline': Icons.workspaces_outline, + 'wrap_text': Icons.wrap_text, + 'wrong_location': Icons.wrong_location, + 'wysiwyg': Icons.wysiwyg, + + // X-Z + 'yard': Icons.yard, + 'youtube_searched_for': Icons.youtube_searched_for, + 'zoom_in': Icons.zoom_in, + 'zoom_in_map': Icons.zoom_in_map, + 'zoom_out': Icons.zoom_out, + 'zoom_out_map': Icons.zoom_out_map, + }; + + // Check if the icon exists + if (iconMap.containsKey(iconName)) { + return Icon( + iconMap[iconName], + size: size, + color: color, + semanticLabel: iconName, + ); + } else { + // Return a fallback icon + return Icon( + Icons.help_outline, + size: size, + color: Colors.grey, + semanticLabel: '$iconName', + ); + } + } +} diff --git a/old/lib/widgets/custom_image_widget.dart b/old/lib/widgets/custom_image_widget.dart new file mode 100644 index 0000000..c444f5b --- /dev/null +++ b/old/lib/widgets/custom_image_widget.dart @@ -0,0 +1,182 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import '../core/app_export.dart'; + +extension ImageTypeExtension on String { + ImageType get imageType { + if (this.startsWith('http') || this.startsWith('https')) { + return ImageType.network; + } else if (this.endsWith('.svg')) { + return ImageType.svg; + } else if (this.startsWith('file: //')) { + return ImageType.file; + } else { + return ImageType.png; + } + } +} + +enum ImageType { svg, png, network, file, unknown } + +// ignore_for_file: must_be_immutable +class CustomImageWidget extends StatelessWidget { + CustomImageWidget({ + this.imageUrl, + this.height, + this.width, + this.color, + this.fit, + this.alignment, + this.onTap, + this.radius, + this.margin, + this.border, + this.placeHolder = 'assets/images/no-image.jpg', + this.errorWidget, + this.semanticLabel, + }); + + ///[imageUrl] is required parameter for showing image + final String? imageUrl; + + final double? height; + + final double? width; + + final BoxFit? fit; + + final String placeHolder; + + final Color? color; + + final Alignment? alignment; + + final VoidCallback? onTap; + + final BorderRadius? radius; + + final EdgeInsetsGeometry? margin; + + final BoxBorder? border; + + /// Optional widget to show when the image fails to load. + /// If null, a default asset image is shown. + final Widget? errorWidget; + + /// Semantic label for the image to improve accessibility + final String? semanticLabel; + + @override + Widget build(BuildContext context) { + return alignment != null + ? Align(alignment: alignment!, child: _buildWidget()) + : _buildWidget(); + } + + Widget _buildWidget() { + return Padding( + padding: margin ?? EdgeInsets.zero, + child: InkWell( + onTap: onTap, + child: _buildCircleImage(), + ), + ); + } + + ///build the image with border radius + _buildCircleImage() { + if (radius != null) { + return ClipRRect( + borderRadius: radius ?? BorderRadius.zero, + child: _buildImageWithBorder(), + ); + } else { + return _buildImageWithBorder(); + } + } + + ///build the image with border and border radius style + _buildImageWithBorder() { + if (border != null) { + return Container( + decoration: BoxDecoration( + border: border, + borderRadius: radius, + ), + child: _buildImageView(), + ); + } else { + return _buildImageView(); + } + } + + Widget _buildImageView() { + if (imageUrl != null) { + switch (imageUrl!.imageType) { + case ImageType.svg: + return Container( + height: height, + width: width, + child: SvgPicture.asset( + imageUrl!, + height: height, + width: width, + fit: fit ?? BoxFit.contain, + colorFilter: this.color != null + ? ColorFilter.mode( + this.color ?? Colors.transparent, BlendMode.srcIn) + : null, + semanticsLabel: semanticLabel, + ), + ); + case ImageType.file: + return Image.file( + File(imageUrl!), + height: height, + width: width, + fit: fit ?? BoxFit.cover, + color: color, + semanticLabel: semanticLabel, + ); + case ImageType.network: + return CachedNetworkImage( + height: height, + width: width, + fit: fit, + imageUrl: imageUrl!, + color: color, + placeholder: (context, url) => Container( + height: 30, + width: 30, + child: LinearProgressIndicator( + color: Colors.grey.shade200, + backgroundColor: Colors.grey.shade100, + ), + ), + errorWidget: (context, url, error) => + errorWidget ?? + Image.asset( + placeHolder, + height: height, + width: width, + fit: fit ?? BoxFit.cover, + semanticLabel: semanticLabel, + ), + ); + case ImageType.png: + default: + return Image.asset( + imageUrl!, + height: height, + width: width, + fit: fit ?? BoxFit.cover, + color: color, + semanticLabel: semanticLabel, + ); + } + } + return SizedBox(); + } +} diff --git a/old/lib/widgets/debug_banner_widget.dart b/old/lib/widgets/debug_banner_widget.dart new file mode 100644 index 0000000..f4b9bd7 --- /dev/null +++ b/old/lib/widgets/debug_banner_widget.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import '../services/supabase_service.dart'; + +class DebugBannerWidget extends StatefulWidget { + final String? lastError; + final int routeCount; + final bool isConnected; + final String connectionStatus; + + const DebugBannerWidget({ + super.key, + this.lastError, + this.routeCount = 0, + this.isConnected = false, + this.connectionStatus = 'Not checked', + }); + + @override + State createState() => _DebugBannerWidgetState(); +} + +class _DebugBannerWidgetState extends State { + bool _isExpanded = false; + + @override + Widget build(BuildContext context) { + return Positioned( + top: 16, + left: 16, + child: GestureDetector( + onTap: () { + setState(() { + _isExpanded = !_isExpanded; + }); + }, + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.8, + ), + decoration: BoxDecoration( + color: Colors.black87, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: const Color(0xFFFEE715), width: 2), + ), + child: _isExpanded ? _buildExpandedView() : _buildCollapsedView(), + ), + ), + ); + } + + Widget _buildCollapsedView() { + return Container( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.bug_report, + color: widget.isConnected ? Colors.green : Colors.red, + size: 16, + ), + const SizedBox(width: 4), + Text( + 'DEBUG', + style: TextStyle( + color: widget.isConnected ? Colors.green : Colors.red, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 4), + const Icon( + Icons.keyboard_arrow_down, + color: Color(0xFFFEE715), + size: 16, + ), + ], + ), + ); + } + + Widget _buildExpandedView() { + final truncatedUrl = SupabaseService.getTruncatedUrl(); + final maskedKey = SupabaseService.getMaskedAnonKey(); + + return Container( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.bug_report, + color: widget.isConnected ? Colors.green : Colors.red, + size: 16, + ), + const SizedBox(width: 4), + const Text( + 'DEBUG INFO', + style: TextStyle( + color: Color(0xFFFEE715), + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + const Icon( + Icons.keyboard_arrow_up, + color: Color(0xFFFEE715), + size: 16, + ), + ], + ), + const Divider(color: Color(0xFFFEE715), height: 16, thickness: 1), + + // Connection Status + _buildDebugRow( + 'Status', + widget.isConnected ? 'Connected ✓' : 'Connection Failed', + isError: !widget.isConnected, + ), + + // Supabase URL (first/last 8 chars) + _buildDebugRow( + 'SUPABASE_URL', + truncatedUrl, + ), + + // Supabase Anon Key (first/last 6 chars, masked) + _buildDebugRow( + 'SUPABASE_ANON_KEY', + maskedKey, + ), + + // Route Count + _buildDebugRow( + 'Routes Count', + widget.isConnected ? '${widget.routeCount} routes' : 'N/A', + ), + + // Last Error + if (widget.lastError != null) ...[ + const SizedBox(height: 8), + _buildDebugRow( + 'Last Error', + widget.lastError!, + isError: true, + ), + ], + ], + ), + ); + } + + Widget _buildDebugRow(String label, String value, {bool isError = false}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '$label:', + style: const TextStyle( + color: Color(0xFFFEE715), + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + value, + style: TextStyle( + color: isError ? Colors.red : Colors.white, + fontSize: 9, + fontFamily: 'monospace', + ), + maxLines: isError ? 3 : 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + } +} diff --git a/old/lib/widgets/route_selection_bottom_sheet.dart b/old/lib/widgets/route_selection_bottom_sheet.dart new file mode 100644 index 0000000..93a3009 --- /dev/null +++ b/old/lib/widgets/route_selection_bottom_sheet.dart @@ -0,0 +1,284 @@ +import 'package:flutter/material.dart'; + +import '../services/app_state_service.dart'; + +class RouteSelectionBottomSheet extends StatefulWidget { + final String title; + final VoidCallback? onRouteChanged; + + const RouteSelectionBottomSheet({ + super.key, + this.title = 'Seleccionar Ruta', + this.onRouteChanged, + }); + + @override + State createState() => + _RouteSelectionBottomSheetState(); +} + +class _RouteSelectionBottomSheetState extends State { + final AppStateService _appStateService = AppStateService(); + bool _isRefreshing = false; + + @override + void initState() { + super.initState(); + _appStateService.addListener(_onStateChanged); + } + + @override + void dispose() { + _appStateService.removeListener(_onStateChanged); + super.dispose(); + } + + void _onStateChanged() { + if (mounted) { + setState(() {}); + } + } + + Future _onRouteSelected(String routeId) async { + try { + await _appStateService.selectRoute(routeId); + + // Notify parent widget that route changed + widget.onRouteChanged?.call(); + + if (mounted) { + Navigator.pop(context); + + // Show success message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('Ruta cambiada: ${_appStateService.selectedRouteName}'), + backgroundColor: Colors.green, + duration: const Duration(seconds: 2), + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error seleccionando ruta: $e'), + backgroundColor: Colors.red, + duration: const Duration(seconds: 3), + ), + ); + } + } + } + + Future _refreshRoutes() async { + setState(() { + _isRefreshing = true; + }); + + try { + await _appStateService.refreshRoutes(); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Rutas actualizadas'), + backgroundColor: Colors.green, + duration: Duration(seconds: 2), + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error actualizando rutas: $e'), + backgroundColor: Colors.red, + duration: const Duration(seconds: 3), + ), + ); + } + } finally { + if (mounted) { + setState(() { + _isRefreshing = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + final routes = _appStateService.allRoutes; + final selectedRouteId = _appStateService.selectedRouteId; + final isLoading = _appStateService.isLoadingRoutes || _isRefreshing; + final error = _appStateService.error; + + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Handle + Container( + width: 40, + height: 4, + margin: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + + // Header + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + Expanded( + child: Text( + widget.title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Color(0xFF101820), + ), + ), + ), + IconButton( + onPressed: _refreshRoutes, + icon: Icon( + Icons.refresh, + color: isLoading ? Colors.grey : const Color(0xFF101820), + ), + ), + ], + ), + ), + + const SizedBox(height: 16), + + // Content + Flexible( + child: isLoading + ? const Center( + child: Padding( + padding: EdgeInsets.all(40), + child: CircularProgressIndicator( + valueColor: + AlwaysStoppedAnimation(Color(0xFFFEE715)), + ), + ), + ) + : error != null + ? Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.error_outline, + color: Colors.red[600], + size: 48, + ), + const SizedBox(height: 16), + Text( + error, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.red[600], + fontSize: 16, + ), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: _refreshRoutes, + icon: const Icon(Icons.refresh), + label: const Text('Reintentar'), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFEE715), + foregroundColor: const Color(0xFF101820), + ), + ), + ], + ), + ) + : routes.isEmpty + ? const Padding( + padding: EdgeInsets.all(40), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.route_outlined, + color: Colors.grey, + size: 48, + ), + SizedBox(height: 16), + Text( + 'No hay rutas disponibles', + style: TextStyle( + fontSize: 16, + color: Colors.grey, + ), + ), + ], + ), + ) + : ListView.builder( + shrinkWrap: true, + itemCount: routes.length, + itemBuilder: (context, index) { + final route = routes[index]; + final isSelected = selectedRouteId == route.id; + + return ListTile( + leading: Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: Color(int.parse(route.color + .replaceFirst('#', '0xFF'))), + shape: BoxShape.circle, + ), + ), + title: Text( + route.displayName, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + subtitle: route.direction.isNotEmpty + ? Text( + route.direction, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ) + : null, + trailing: isSelected + ? const Icon( + Icons.check_circle, + color: Color(0xFFFEE715), + ) + : null, + onTap: () => _onRouteSelected(route.id), + ); + }, + ), + ), + + const SizedBox(height: 16), + ], + ), + ), + ); + } +} diff --git a/old/pubspec.yaml b/old/pubspec.yaml new file mode 100644 index 0000000..121fea3 --- /dev/null +++ b/old/pubspec.yaml @@ -0,0 +1,52 @@ +name: sibu +description: A new Flutter project. +publish_to: none +version: 1.0.0+1 + +environment: + sdk: ^3.6.0 + +dependencies: + flutter: # CRITICAL: Required for every Flutter project - DO NOT REMOVE + sdk: flutter # CRITICAL: Required for every Flutter project - DO NOT REMOVE + + # CRITICAL: Core UI and responsive design - DO NOT REMOVE OR MODIFY + sizer: ^2.0.15 # Required for responsive design system + flutter_svg: ^2.0.9 # Required for SVG icon support + google_fonts: ^6.1.0 # Required for typography (replaces local fonts) + shared_preferences: ^2.5.3 # Required for local data storage + web: ^1.1.1 + + # Feature dependencies - safe to modify + cached_network_image: ^3.3.1 + connectivity_plus: ^6.1.4 + dio: ^5.4.0 + fluttertoast: ^8.2.4 + fl_chart: ^0.65.0 + camera: ^0.10.5+5 + image_picker: ^1.0.4 + permission_handler: ^11.1.0 + google_maps_flutter: ^2.12.3 + geolocator: ^13.0.4 + supabase_flutter: ^2.10.3 + url_launcher: ^6.3.2 + intl: ^0.20.2 + + universal_html: ^2.2.4 +dev_dependencies: + flutter_test: # CRITICAL: Required for Flutter project testing - DO NOT REMOVE + sdk: flutter # CRITICAL: Required for Flutter project testing - DO NOT REMOVE + flutter_lints: ^5.0.0 # CRITICAL: Required for code quality - DO NOT REMOVE + +flutter: + uses-material-design: true # CRITICAL: Required for Material icon font - DO NOT REMOVE + assets: + - assets/ + - assets/images/ + # CRITICAL ASSET MANAGEMENT RULES: + # - DO NOT ADD NEW ASSET DIRECTORIES (assets/svg/, assets/icons/, etc.) + # - ONLY USE EXISTING AND ITEMS AVAILABLE IN THE DIRECTORIES LISTED ABOVE (assets/, assets/images/) + + # CRITICAL FONTS RULE: + # - THIS PROJECT USES GOOGLE FONTS INSTEAD OF LOCAL FONTS + # - DO NOT ADD ANY LOCAL FONTS SECTION OR FONT FILES diff --git a/old/scripts/get-local-credentials.sh b/old/scripts/get-local-credentials.sh new file mode 100644 index 0000000..4c8ff1a --- /dev/null +++ b/old/scripts/get-local-credentials.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Get and display local Supabase credentials +# Useful for manual configuration or debugging + +set -e + +cd "$(dirname "$0")/.." + +if ! supabase status > /dev/null 2>&1; then + echo "❌ Supabase is not running locally." + echo " Please run: ./scripts/setup-local-supabase.sh" + exit 1 +fi + +echo "📋 Local Supabase Credentials" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━-Defaults: supabase/config.toml +supabase status + +echo "" +echo "💡 Use these credentials with Flutter:" +echo " flutter run -d chrome \\" +echo " --dart-define=SUPABASE_URL=\"\" \\" +echo " --dart-define=SUPABASE_ANON_KEY=\"\"" + diff --git a/old/scripts/run-flutter-backend.sh b/old/scripts/run-flutter-backend.sh new file mode 100644 index 0000000..09e4f9a --- /dev/null +++ b/old/scripts/run-flutter-backend.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Run Flutter app with backend API connection +# This script runs Flutter with the backend API URL configured + +set -e + +# Navigate to project root +cd "$(dirname "$0")/.." + +# Get API URL from environment or use default +API_BASE_URL="${API_BASE_URL:-http://localhost:8000}" + +# Determine device (default to chrome for web) +DEVICE="${1:-chrome}" + +echo "🚀 Running Flutter app with backend API..." +echo " API URL: $API_BASE_URL" +echo " Device: $DEVICE" +echo "" + +# Run Flutter with environment variables +flutter run -d "$DEVICE" \ + --dart-define=API_BASE_URL="$API_BASE_URL" + diff --git a/old/scripts/run-flutter-local.sh b/old/scripts/run-flutter-local.sh new file mode 100644 index 0000000..e2bb52d --- /dev/null +++ b/old/scripts/run-flutter-local.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# Run Flutter app with local Supabase credentials +# This script extracts local Supabase credentials and runs Flutter with them + +set -e + +# Navigate to project root +cd "$(dirname "$0")/.." + +# Check if Supabase is running +if ! supabase status > /dev/null 2>&1; then + echo "❌ Supabase is not running locally." + echo " Please run: ./scripts/setup-local-supabase.sh" + exit 1 +fi + +# Get Supabase credentials from status +echo "🔍 Getting local Supabase credentials..." + +# Get status output +STATUS_OUTPUT=$(supabase status 2>/dev/null) + +# Extract URL - look for "API URL" line +SUPABASE_URL=$(echo "$STATUS_OUTPUT" | grep -i "API URL" | sed -E 's/.*(http[^[:space:]]+).*/\1/' | head -1) + +# Extract anon key - look for "anon key" line +SUPABASE_ANON_KEY=$(echo "$STATUS_OUTPUT" | grep -i "anon key" | sed -E 's/.*anon key[[:space:]]+([^[:space:]]+).*/\1/' | head -1) + +# Alternative: try JSON output if available +if [ -z "$SUPABASE_URL" ] || [ -z "$SUPABASE_ANON_KEY" ]; then + JSON_OUTPUT=$(supabase status --output json 2>/dev/null || echo "") + if [ -n "$JSON_OUTPUT" ]; then + SUPABASE_URL=$(echo "$JSON_OUTPUT" | grep -o '"API URL":"[^"]*' | sed 's/"API URL":"//' | head -1) + SUPABASE_ANON_KEY=$(echo "$JSON_OUTPUT" | grep -o '"anon key":"[^"]*' | sed 's/"anon key":"//' | head -1) + fi +fi + +# If still empty, try reading from .env.local or config +if [ -z "$SUPABASE_URL" ] || [ -z "$SUPABASE_ANON_KEY" ]; then + echo "⚠️ Could not automatically extract credentials." + echo " Please run 'supabase status' and manually set:" + echo " SUPABASE_URL and SUPABASE_ANON_KEY" + echo "" + echo " Then run Flutter with:" + echo " flutter run -d chrome --dart-define=SUPABASE_URL= --dart-define=SUPABASE_ANON_KEY=" + exit 1 +fi + +echo "✅ Found local Supabase credentials" +echo " URL: $SUPABASE_URL" +echo " Key: ${SUPABASE_ANON_KEY:0:20}..." +echo "" + +# Determine device (default to chrome for web) +DEVICE="${1:-chrome}" + +echo "🚀 Running Flutter app with local Supabase..." +echo "" + +# Run Flutter with environment variables +flutter run -d "$DEVICE" \ + --dart-define=SUPABASE_URL="$SUPABASE_URL" \ + --dart-define=SUPABASE_ANON_KEY="$SUPABASE_ANON_KEY" + diff --git a/old/scripts/run-flutter-supabase.sh b/old/scripts/run-flutter-supabase.sh new file mode 100644 index 0000000..c4655ab --- /dev/null +++ b/old/scripts/run-flutter-supabase.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# Run Flutter app with Supabase credentials from env.json +# This script reads env.json and passes Supabase credentials to Flutter + +set -e + +# Navigate to project root +cd "$(dirname "$0")/.." + +# Check if env.json exists +if [ ! -f "env.json" ]; then + echo "❌ env.json not found!" + echo " Please create env.json with SUPABASE_URL and SUPABASE_ANON_KEY" + exit 1 +fi + +# Extract Supabase credentials from env.json +# Using Python for reliable JSON parsing +SUPABASE_URL=$(python3 -c "import json; print(json.load(open('env.json'))['SUPABASE_URL'])" 2>/dev/null || echo "") +SUPABASE_ANON_KEY=$(python3 -c "import json; print(json.load(open('env.json'))['SUPABASE_ANON_KEY'])" 2>/dev/null || echo "") +GOOGLE_MAPS_API_KEY=$(python3 -c "import json; print(json.load(open('env.json'))['GOOGLE_MAPS_API_KEY'])" 2>/dev/null || echo "") + +# Fallback: try with node if python fails +if [ -z "$SUPABASE_URL" ] || [ -z "$SUPABASE_ANON_KEY" ]; then + if command -v node >/dev/null 2>&1; then + SUPABASE_URL=$(node -e "const fs=require('fs'); const data=JSON.parse(fs.readFileSync('env.json')); console.log(data.SUPABASE_URL);" 2>/dev/null || echo "") + SUPABASE_ANON_KEY=$(node -e "const fs=require('fs'); const data=JSON.parse(fs.readFileSync('env.json')); console.log(data.SUPABASE_ANON_KEY);" 2>/dev/null || echo "") + GOOGLE_MAPS_API_KEY=$(node -e "const fs=require('fs'); const data=JSON.parse(fs.readFileSync('env.json')); console.log(data.GOOGLE_MAPS_API_KEY);" 2>/dev/null || echo "") + fi +fi + +# Check if we got the credentials +if [ -z "$SUPABASE_URL" ] || [ -z "$SUPABASE_ANON_KEY" ]; then + echo "❌ Could not extract Supabase credentials from env.json" + echo " Make sure env.json contains SUPABASE_URL and SUPABASE_ANON_KEY" + exit 1 +fi + +echo "✅ Found Supabase credentials" +echo " URL: $SUPABASE_URL" +echo " Key: ${SUPABASE_ANON_KEY:0:20}..." +if [ -n "$GOOGLE_MAPS_API_KEY" ] && [ "$GOOGLE_MAPS_API_KEY" != "your-google-maps-api-key" ]; then + echo "🗺️ Found Google Maps API Key" + # Inject into web/index.html + if [ -f "web/index.html" ]; then + sed -i '' "s/key=[^\&\"]*/key=$GOOGLE_MAPS_API_KEY/" web/index.html + fi +fi +echo "" + +# Determine device (default to web-server for web) +DEVICE="${1:-web-server}" + +echo "🚀 Running Flutter app with Supabase..." +echo " Device: $DEVICE" +echo "" + +# Run Flutter with environment variables +flutter run -d "$DEVICE" \ + --dart-define=SUPABASE_URL="$SUPABASE_URL" \ + --dart-define=SUPABASE_ANON_KEY="$SUPABASE_ANON_KEY" \ + --dart-define=GOOGLE_MAPS_API_KEY="$GOOGLE_MAPS_API_KEY" + diff --git a/old/scripts/setup-local-supabase.sh b/old/scripts/setup-local-supabase.sh new file mode 100644 index 0000000..225e47f --- /dev/null +++ b/old/scripts/setup-local-supabase.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Setup script for local Supabase development +# This script initializes and starts Supabase locally + +set -e + +echo "🚀 Setting up local Supabase..." + +# Check if Docker is running +if ! docker info > /dev/null 2>&1; then + echo "❌ Docker is not running. Please start Docker Desktop first." + echo " Visit: https://docs.docker.com/desktop" + exit 1 +fi + +# Navigate to project root +cd "$(dirname "$0")/.." + +# Check if Supabase is initialized +if [ ! -f "supabase/config.toml" ]; then + echo "📦 Initializing Supabase..." + supabase init +fi + +# Start Supabase +echo "🔄 Starting Supabase local instance..." +supabase start + +# Wait a moment for services to be ready +sleep 3 + +# Get credentials +echo "" +echo "✅ Supabase is running locally!" +echo "" +echo "📋 Local Supabase Credentials:" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━-Defaults: supabase/config.toml +supabase status + +echo "" +echo "💡 To stop Supabase, run: supabase stop" +echo "💡 To view logs, run: supabase logs" +echo "💡 To reset database, run: supabase db reset" + diff --git a/old/supabase/.gitignore b/old/supabase/.gitignore new file mode 100644 index 0000000..ad9264f --- /dev/null +++ b/old/supabase/.gitignore @@ -0,0 +1,8 @@ +# Supabase +.branches +.temp + +# dotenvx +.env.keys +.env.local +.env.*.local diff --git a/old/supabase/config.toml b/old/supabase/config.toml new file mode 100644 index 0000000..16a2b99 --- /dev/null +++ b/old/supabase/config.toml @@ -0,0 +1,357 @@ +# For detailed configuration reference documentation, visit: +# https://supabase.com/docs/guides/local-development/cli/config +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "old" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. `public` and `graphql_public` schemas are included by default. +schemas = ["public", "graphql_public"] +# Extra schemas to add to the search_path of every request. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 + +[api.tls] +# Enable HTTPS endpoints locally using a self-signed certificate. +enabled = false +# Paths to self-signed certificate pair. +# cert_path = "../certs/my-cert.pem" +# key_path = "../certs/my-key.pem" + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 17 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +# [db.vault] +# secret_key = "env(SECRET_VALUE)" + +[db.migrations] +# If disabled, migrations will be skipped during a db push or reset. +enabled = true +# Specifies an ordered list of schema files that describe your database. +# Supports glob patterns relative to supabase directory: "./schemas/*.sql" +schema_paths = [] + +[db.seed] +# If enabled, seeds the database after migrations during a db reset. +enabled = true +# Specifies an ordered list of seed files to load during db reset. +# Supports glob patterns relative to supabase directory: "./seeds/*.sql" +sql_paths = ["./seed.sql"] + +[db.network_restrictions] +# Enable management of network restrictions. +enabled = false +# List of IPv4 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv4 connections. Set empty array to block all IPs. +allowed_cidrs = ["0.0.0.0/0"] +# List of IPv6 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv6 connections. Set empty array to block all IPs. +allowed_cidrs_v6 = ["::/0"] + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv4) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 +# admin_email = "admin@email.com" +# sender_name = "Admin" + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +# Image transformation API is available to Supabase Pro plan. +# [storage.image_transformation] +# enabled = true + +# Uncomment to configure local storage buckets +# [storage.buckets.images] +# public = false +# file_size_limit = "50MiB" +# allowed_mime_types = ["image/png", "image/jpeg"] +# objects_path = "./images" + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:/auth/v1). +# jwt_issuer = "" +# Path to JWT signing key. DO NOT commit your signing keys file to git. +# signing_keys_path = "./signing_keys.json" +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow anonymous sign-ins to your project. +enable_anonymous_sign_ins = false +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false +# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. +minimum_password_length = 6 +# Passwords that do not meet the following requirements will be rejected as weak. Supported values +# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` +password_requirements = "" + +[auth.rate_limit] +# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. +email_sent = 2 +# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. +sms_sent = 30 +# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. +anonymous_users = 30 +# Number of sessions that can be refreshed in a 5 minute interval per IP address. +token_refresh = 150 +# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). +sign_in_sign_ups = 30 +# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. +token_verifications = 30 +# Number of Web3 logins that can be made in a 5 minute interval per IP address. +web3 = 30 + +# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. +# [auth.captcha] +# enabled = true +# provider = "hcaptcha" +# secret = "" + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false +# If enabled, users will need to reauthenticate or have logged in recently to change their password. +secure_password_change = false +# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. +max_frequency = "1s" +# Number of characters used in the email OTP. +otp_length = 6 +# Number of seconds before the email OTP expires (defaults to 1 hour). +otp_expiry = 3600 + +# Use a production-ready SMTP server +# [auth.email.smtp] +# enabled = true +# host = "smtp.sendgrid.net" +# port = 587 +# user = "apikey" +# pass = "env(SENDGRID_API_KEY)" +# admin_email = "admin@email.com" +# sender_name = "Admin" + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +# Uncomment to customize notification email template +# [auth.email.notification.password_changed] +# enabled = true +# subject = "Your password has been changed" +# content_path = "./templates/password_changed_notification.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = false +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ .Code }}" +# Controls the minimum amount of time that must pass before sending another sms otp. +max_frequency = "5s" + +# Use pre-defined map of phone number to OTP for testing. +# [auth.sms.test_otp] +# 4152127777 = "123456" + +# Configure logged in session timeouts. +# [auth.sessions] +# Force log out after the specified duration. +# timebox = "24h" +# Force log out if the user has been inactive longer than the specified duration. +# inactivity_timeout = "8h" + +# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. +# [auth.hook.before_user_created] +# enabled = true +# uri = "pg-functions://postgres/auth/before-user-created-hook" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +# [auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions:////" + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Multi-factor-authentication is available to Supabase Pro plan. +[auth.mfa] +# Control how many MFA factors can be enrolled at once per user. +max_enrolled_factors = 10 + +# Control MFA via App Authenticator (TOTP) +[auth.mfa.totp] +enroll_enabled = false +verify_enabled = false + +# Configure MFA via Phone Messaging +[auth.mfa.phone] +enroll_enabled = false +verify_enabled = false +otp_length = 6 +template = "Your code is {{ .Code }}" +max_frequency = "5s" + +# Configure MFA via WebAuthn +# [auth.mfa.web_authn] +# enroll_enabled = true +# verify_enabled = true + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth redirectUrl. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" +# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. +skip_nonce_check = false +# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address. +email_optional = false + +# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. +# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. +[auth.web3.solana] +enabled = false + +# Use Firebase Auth as a third-party provider alongside Supabase Auth. +[auth.third_party.firebase] +enabled = false +# project_id = "my-firebase-project" + +# Use Auth0 as a third-party provider alongside Supabase Auth. +[auth.third_party.auth0] +enabled = false +# tenant = "my-auth0-tenant" +# tenant_region = "us" + +# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. +[auth.third_party.aws_cognito] +enabled = false +# user_pool_id = "my-user-pool-id" +# user_pool_region = "us-east-1" + +# Use Clerk as a third-party provider alongside Supabase Auth. +[auth.third_party.clerk] +enabled = false +# Obtain from https://clerk.com/setup/supabase +# domain = "example.clerk.accounts.dev" + +# OAuth server configuration +[auth.oauth_server] +# Enable OAuth server functionality +enabled = false +# Path for OAuth consent flow UI +authorization_url_path = "/oauth/consent" +# Allow dynamic client registration +allow_dynamic_registration = false + +[edge_runtime] +enabled = true +# Supported request policies: `oneshot`, `per_worker`. +# `per_worker` (default) — enables hot reload during local development. +# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks). +policy = "per_worker" +# Port to attach the Chrome inspector for debugging edge functions. +inspector_port = 8083 +# The Deno major version to use. +deno_version = 2 + +# [edge_runtime.secrets] +# secret_key = "env(SECRET_VALUE)" + +[analytics] +enabled = true +port = 54327 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. .s3-.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" diff --git a/old/supabase/migrations/20241019215951_sibu_transportation_system.sql b/old/supabase/migrations/20241019215951_sibu_transportation_system.sql new file mode 100644 index 0000000..cacf266 --- /dev/null +++ b/old/supabase/migrations/20241019215951_sibu_transportation_system.sql @@ -0,0 +1,445 @@ +-- Location: supabase/migrations/20241019215951_sibu_transportation_system.sql +-- Schema Analysis: Fresh project with no existing database objects +-- Integration Type: Complete new schema for SIBU transportation system +-- Dependencies: New transportation management module + +-- 1. Extensions & Custom Types +CREATE TYPE public.route_status AS ENUM ('active', 'inactive', 'maintenance'); +CREATE TYPE public.stop_type AS ENUM ('terminal', 'regular', 'express_only'); +CREATE TYPE public.bus_schedule_type AS ENUM ('weekday', 'weekend', 'holiday'); + +-- 2. Core Tables (no foreign keys) + +-- Routes table - stores bus route information +CREATE TABLE public.routes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL UNIQUE, -- e.g., "Boquete>David", "David>Boquete" + description TEXT, + origin_city TEXT NOT NULL, + destination_city TEXT NOT NULL, + distance_km DECIMAL(6,2), + estimated_duration_minutes INTEGER, + status public.route_status DEFAULT 'active'::public.route_status, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +-- Bus stops table +CREATE TABLE public.bus_stops ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + latitude DECIMAL(10,8) NOT NULL, + longitude DECIMAL(11,8) NOT NULL, + city TEXT NOT NULL, + address TEXT, + stop_type public.stop_type DEFAULT 'regular'::public.stop_type, + has_shelter BOOLEAN DEFAULT false, + has_seating BOOLEAN DEFAULT false, + is_accessible BOOLEAN DEFAULT false, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +-- 3. Dependent Tables (with foreign keys) + +-- Route stops - junction table connecting routes to their stops +CREATE TABLE public.route_stops ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + route_id UUID REFERENCES public.routes(id) ON DELETE CASCADE, + stop_id UUID REFERENCES public.bus_stops(id) ON DELETE CASCADE, + stop_order INTEGER NOT NULL, -- Order of stop in the route (1, 2, 3...) + travel_time_minutes INTEGER, -- Time from previous stop + is_pickup_point BOOLEAN DEFAULT true, + is_dropoff_point BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +-- Bus schedules table +CREATE TABLE public.bus_schedules ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + route_id UUID REFERENCES public.routes(id) ON DELETE CASCADE, + departure_time TIME NOT NULL, + frequency_minutes INTEGER DEFAULT 30, -- How often bus runs (every 30 minutes) + schedule_type public.bus_schedule_type DEFAULT 'weekday'::public.bus_schedule_type, + is_active BOOLEAN DEFAULT true, + notes TEXT, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +-- Real-time bus tracking (for future implementation) +CREATE TABLE public.bus_tracking ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + route_id UUID REFERENCES public.routes(id) ON DELETE CASCADE, + current_stop_id UUID REFERENCES public.bus_stops(id) ON DELETE SET NULL, + next_stop_id UUID REFERENCES public.bus_stops(id) ON DELETE SET NULL, + estimated_arrival_time TIMESTAMPTZ, + delay_minutes INTEGER DEFAULT 0, + bus_number TEXT, + is_active BOOLEAN DEFAULT true, + last_updated TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +-- 4. Indexes for Performance +CREATE INDEX idx_routes_origin_destination ON public.routes(origin_city, destination_city); +CREATE INDEX idx_routes_status ON public.routes(status); +CREATE INDEX idx_bus_stops_city ON public.bus_stops(city); +CREATE INDEX idx_bus_stops_location ON public.bus_stops(latitude, longitude); +CREATE INDEX idx_route_stops_route_id ON public.route_stops(route_id); +CREATE INDEX idx_route_stops_stop_id ON public.route_stops(stop_id); +CREATE INDEX idx_route_stops_order ON public.route_stops(route_id, stop_order); +CREATE INDEX idx_bus_schedules_route_id ON public.bus_schedules(route_id); +CREATE INDEX idx_bus_schedules_departure_time ON public.bus_schedules(departure_time); +CREATE INDEX idx_bus_tracking_route_id ON public.bus_tracking(route_id); +CREATE INDEX idx_bus_tracking_active ON public.bus_tracking(is_active); + +-- 5. Helper Functions (MUST BE BEFORE RLS POLICIES) +CREATE OR REPLACE FUNCTION public.calculate_next_bus_arrival( + p_route_id UUID, + p_stop_id UUID, + p_current_time TIME DEFAULT CURRENT_TIME +) +RETURNS TABLE( + next_departure TIME, + estimated_arrival_time TIMESTAMPTZ, + minutes_until_arrival INTEGER +) +LANGUAGE plpgsql +STABLE +SECURITY DEFINER +AS $$ +DECLARE + avg_speed_kmh DECIMAL := 65.0; -- Average bus speed + stop_dwell_time INTEGER := 2; -- Minutes per stop + route_distance DECIMAL; + stop_position INTEGER; + time_to_stop INTEGER; + next_schedule_time TIME; +BEGIN + -- Get the next scheduled departure + SELECT bs.departure_time INTO next_schedule_time + FROM public.bus_schedules bs + WHERE bs.route_id = p_route_id + AND bs.is_active = true + AND bs.departure_time > p_current_time + ORDER BY bs.departure_time ASC + LIMIT 1; + + -- If no more schedules today, get first schedule tomorrow + IF next_schedule_time IS NULL THEN + SELECT bs.departure_time INTO next_schedule_time + FROM public.bus_schedules bs + WHERE bs.route_id = p_route_id + AND bs.is_active = true + ORDER BY bs.departure_time ASC + LIMIT 1; + END IF; + + -- Get stop position in route and calculate travel time + SELECT rs.stop_order INTO stop_position + FROM public.route_stops rs + WHERE rs.route_id = p_route_id AND rs.stop_id = p_stop_id; + + -- Calculate estimated travel time to this stop (simplified calculation) + time_to_stop := (stop_position - 1) * stop_dwell_time + (stop_position * 3); -- ~3 min between stops + + RETURN QUERY SELECT + next_schedule_time, + (CURRENT_DATE + next_schedule_time + (time_to_stop || ' minutes')::INTERVAL)::TIMESTAMPTZ, + EXTRACT(EPOCH FROM ( + (CURRENT_DATE + next_schedule_time + (time_to_stop || ' minutes')::INTERVAL) - NOW() + ))::INTEGER / 60; +END; +$$; + +CREATE OR REPLACE FUNCTION public.get_route_stops_ordered(p_route_id UUID) +RETURNS TABLE( + stop_id UUID, + stop_name TEXT, + latitude DECIMAL, + longitude DECIMAL, + stop_order INTEGER, + travel_time_minutes INTEGER +) +LANGUAGE sql +STABLE +SECURITY DEFINER +AS $$ +SELECT + bs.id, + bs.name, + bs.latitude, + bs.longitude, + rs.stop_order, + rs.travel_time_minutes +FROM public.route_stops rs +JOIN public.bus_stops bs ON rs.stop_id = bs.id +WHERE rs.route_id = p_route_id +ORDER BY rs.stop_order; +$$; + +-- 6. Enable RLS +ALTER TABLE public.routes ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.bus_stops ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.route_stops ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.bus_schedules ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.bus_tracking ENABLE ROW LEVEL SECURITY; + +-- 7. RLS Policies (Pattern 4: Public Read for Transportation Data) +-- All transportation data is publicly readable +CREATE POLICY "public_can_read_routes" ON public.routes + FOR SELECT TO public USING (true); + +CREATE POLICY "public_can_read_bus_stops" ON public.bus_stops + FOR SELECT TO public USING (true); + +CREATE POLICY "public_can_read_route_stops" ON public.route_stops + FOR SELECT TO public USING (true); + +CREATE POLICY "public_can_read_bus_schedules" ON public.bus_schedules + FOR SELECT TO public USING (true); + +CREATE POLICY "public_can_read_bus_tracking" ON public.bus_tracking + FOR SELECT TO public USING (true); + +-- Admin policies for data management (for future admin features) +CREATE POLICY "admin_manage_routes" ON public.routes + FOR ALL TO authenticated USING (false) WITH CHECK (false); -- Disabled for now + +CREATE POLICY "admin_manage_bus_stops" ON public.bus_stops + FOR ALL TO authenticated USING (false) WITH CHECK (false); -- Disabled for now + +-- 8. Triggers for updated_at timestamps +CREATE OR REPLACE FUNCTION public.update_updated_at_column() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$; + +CREATE TRIGGER update_routes_updated_at + BEFORE UPDATE ON public.routes + FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + +CREATE TRIGGER update_bus_stops_updated_at + BEFORE UPDATE ON public.bus_stops + FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + +-- 9. Mock Data for SIBU Transportation System +DO $$ +DECLARE + -- Route IDs + boquete_david_id UUID := gen_random_uuid(); + david_boquete_id UUID := gen_random_uuid(); + palmira_david_id UUID := gen_random_uuid(); + david_palmira_id UUID := gen_random_uuid(); + caldera_david_id UUID := gen_random_uuid(); + david_caldera_id UUID := gen_random_uuid(); + + -- Stop IDs for Boquete area + terminal_boquete_id UUID := gen_random_uuid(); + centro_boquete_id UUID := gen_random_uuid(); + parque_boquete_id UUID := gen_random_uuid(); + hospital_boquete_id UUID := gen_random_uuid(); + escuela_boquete_id UUID := gen_random_uuid(); + mercado_boquete_id UUID := gen_random_uuid(); + + -- Stop IDs for David area + terminal_david_id UUID := gen_random_uuid(); + centro_david_id UUID := gen_random_uuid(); + hospital_david_id UUID := gen_random_uuid(); + mall_david_id UUID := gen_random_uuid(); + + -- Stop IDs for Palmira + centro_palmira_id UUID := gen_random_uuid(); + escuela_palmira_id UUID := gen_random_uuid(); + + -- Stop IDs for Caldera + puerto_caldera_id UUID := gen_random_uuid(); + centro_caldera_id UUID := gen_random_uuid(); +BEGIN + -- Insert Routes + INSERT INTO public.routes (id, name, description, origin_city, destination_city, distance_km, estimated_duration_minutes, status) VALUES + (boquete_david_id, 'Boquete>David', 'Ruta desde Boquete hacia David con paradas principales', 'Boquete', 'David', 38.5, 45, 'active'), + (david_boquete_id, 'David>Boquete', 'Ruta desde David hacia Boquete con paradas principales', 'David', 'Boquete', 38.5, 45, 'active'), + (palmira_david_id, 'Palmira>David', 'Ruta desde Palmira hacia David', 'Palmira', 'David', 25.2, 35, 'active'), + (david_palmira_id, 'David>Palmira', 'Ruta desde David hacia Palmira', 'David', 'Palmira', 25.2, 35, 'active'), + (caldera_david_id, 'Caldera>David', 'Ruta desde Caldera hacia David', 'Caldera', 'David', 42.8, 50, 'active'), + (david_caldera_id, 'David>Caldera', 'Ruta desde David hacia Caldera', 'David', 'Caldera', 42.8, 50, 'active'); + + -- Insert Bus Stops + INSERT INTO public.bus_stops (id, name, latitude, longitude, city, address, stop_type, has_shelter, has_seating) VALUES + -- Boquete stops + (terminal_boquete_id, 'Terminal de Boquete', 8.7697, -82.4328, 'Boquete', 'Centro de Boquete', 'terminal', true, true), + (centro_boquete_id, 'Centro de Boquete', 8.7720, -82.4315, 'Boquete', 'Calle Central', 'regular', true, true), + (parque_boquete_id, 'Parque Central Boquete', 8.7705, -82.4340, 'Boquete', 'Junto al Parque Central', 'regular', false, true), + (hospital_boquete_id, 'Hospital de Boquete', 8.7680, -82.4350, 'Boquete', 'Hospital Regional', 'regular', true, true), + (escuela_boquete_id, 'Escuela Primaria Boquete', 8.7740, -82.4300, 'Boquete', 'Zona Educativa', 'regular', false, false), + (mercado_boquete_id, 'Mercado Municipal Boquete', 8.7715, -82.4365, 'Boquete', 'Mercado Central', 'regular', true, true), + + -- David stops + (terminal_david_id, 'Terminal de David', 8.4177, -82.4270, 'David', 'Terminal de Transporte', 'terminal', true, true), + (centro_david_id, 'Centro de David', 8.4194, -82.4255, 'David', 'Parque Cervantes', 'regular', true, true), + (hospital_david_id, 'Hospital Chiriquí', 8.4156, -82.4289, 'David', 'Complejo Hospitalario', 'regular', true, true), + (mall_david_id, 'Chiriquí Mall', 8.4089, -82.4178, 'David', 'Centro Comercial', 'regular', true, true), + + -- Palmira stops + (centro_palmira_id, 'Centro de Palmira', 8.3544, -82.3611, 'Palmira', 'Plaza Central', 'regular', true, true), + (escuela_palmira_id, 'Escuela de Palmira', 8.3567, -82.3598, 'Palmira', 'Zona Escolar', 'regular', false, true), + + -- Caldera stops + (puerto_caldera_id, 'Puerto de Caldera', 8.2456, -81.7234, 'Caldera', 'Zona Portuaria', 'terminal', true, true), + (centro_caldera_id, 'Centro de Caldera', 8.2478, -81.7198, 'Caldera', 'Centro del Pueblo', 'regular', true, false); + + -- Insert Route Stops (Boquete > David) + INSERT INTO public.route_stops (route_id, stop_id, stop_order, travel_time_minutes, is_pickup_point, is_dropoff_point) VALUES + (boquete_david_id, terminal_boquete_id, 1, 0, true, false), + (boquete_david_id, centro_boquete_id, 2, 3, true, true), + (boquete_david_id, parque_boquete_id, 3, 2, true, true), + (boquete_david_id, hospital_boquete_id, 4, 3, true, true), + (boquete_david_id, mall_david_id, 5, 35, true, true), + (boquete_david_id, centro_david_id, 6, 5, true, true), + (boquete_david_id, terminal_david_id, 7, 3, false, true); + + -- Insert Route Stops (David > Boquete) + INSERT INTO public.route_stops (route_id, stop_id, stop_order, travel_time_minutes, is_pickup_point, is_dropoff_point) VALUES + (david_boquete_id, terminal_david_id, 1, 0, true, false), + (david_boquete_id, centro_david_id, 2, 3, true, true), + (david_boquete_id, mall_david_id, 3, 5, true, true), + (david_boquete_id, hospital_boquete_id, 4, 35, true, true), + (david_boquete_id, parque_boquete_id, 5, 3, true, true), + (david_boquete_id, centro_boquete_id, 6, 2, true, true), + (david_boquete_id, terminal_boquete_id, 7, 3, false, true); + + -- Insert Route Stops (Palmira > David) + INSERT INTO public.route_stops (route_id, stop_id, stop_order, travel_time_minutes, is_pickup_point, is_dropoff_point) VALUES + (palmira_david_id, centro_palmira_id, 1, 0, true, false), + (palmira_david_id, escuela_palmira_id, 2, 2, true, true), + (palmira_david_id, hospital_david_id, 3, 25, true, true), + (palmira_david_id, centro_david_id, 4, 5, true, true), + (palmira_david_id, terminal_david_id, 5, 3, false, true); + + -- Insert Route Stops (David > Palmira) + INSERT INTO public.route_stops (route_id, stop_id, stop_order, travel_time_minutes, is_pickup_point, is_dropoff_point) VALUES + (david_palmira_id, terminal_david_id, 1, 0, true, false), + (david_palmira_id, centro_david_id, 2, 3, true, true), + (david_palmira_id, hospital_david_id, 3, 5, true, true), + (david_palmira_id, escuela_palmira_id, 4, 25, true, true), + (david_palmira_id, centro_palmira_id, 5, 2, false, true); + + -- Insert Route Stops (Caldera > David) + INSERT INTO public.route_stops (route_id, stop_id, stop_order, travel_time_minutes, is_pickup_point, is_dropoff_point) VALUES + (caldera_david_id, puerto_caldera_id, 1, 0, true, false), + (caldera_david_id, centro_caldera_id, 2, 3, true, true), + (caldera_david_id, hospital_david_id, 3, 40, true, true), + (caldera_david_id, centro_david_id, 4, 5, true, true), + (caldera_david_id, terminal_david_id, 5, 3, false, true); + + -- Insert Route Stops (David > Caldera) + INSERT INTO public.route_stops (route_id, stop_id, stop_order, travel_time_minutes, is_pickup_point, is_dropoff_point) VALUES + (david_caldera_id, terminal_david_id, 1, 0, true, false), + (david_caldera_id, centro_david_id, 2, 3, true, true), + (david_caldera_id, hospital_david_id, 3, 5, true, true), + (david_caldera_id, centro_caldera_id, 4, 40, true, true), + (david_caldera_id, puerto_caldera_id, 5, 3, false, true); + + -- Insert Bus Schedules + INSERT INTO public.bus_schedules (route_id, departure_time, frequency_minutes, schedule_type, is_active) VALUES + -- Boquete > David (every 30 minutes from 5:00 to 20:00) + (boquete_david_id, '05:00:00', 30, 'weekday', true), + (boquete_david_id, '05:30:00', 30, 'weekday', true), + (boquete_david_id, '06:00:00', 30, 'weekday', true), + (boquete_david_id, '06:30:00', 30, 'weekday', true), + (boquete_david_id, '07:00:00', 30, 'weekday', true), + (boquete_david_id, '07:30:00', 30, 'weekday', true), + (boquete_david_id, '08:00:00', 30, 'weekday', true), + (boquete_david_id, '08:30:00', 30, 'weekday', true), + (boquete_david_id, '09:00:00', 30, 'weekday', true), + (boquete_david_id, '09:30:00', 30, 'weekday', true), + (boquete_david_id, '10:00:00', 30, 'weekday', true), + (boquete_david_id, '10:30:00', 30, 'weekday', true), + (boquete_david_id, '11:00:00', 30, 'weekday', true), + (boquete_david_id, '11:30:00', 30, 'weekday', true), + (boquete_david_id, '12:00:00', 30, 'weekday', true), + (boquete_david_id, '12:30:00', 30, 'weekday', true), + (boquete_david_id, '13:00:00', 30, 'weekday', true), + (boquete_david_id, '13:30:00', 30, 'weekday', true), + (boquete_david_id, '14:00:00', 30, 'weekday', true), + (boquete_david_id, '14:30:00', 30, 'weekday', true), + (boquete_david_id, '15:00:00', 30, 'weekday', true), + (boquete_david_id, '15:30:00', 30, 'weekday', true), + (boquete_david_id, '16:00:00', 30, 'weekday', true), + (boquete_david_id, '16:30:00', 30, 'weekday', true), + (boquete_david_id, '17:00:00', 30, 'weekday', true), + (boquete_david_id, '17:30:00', 30, 'weekday', true), + (boquete_david_id, '18:00:00', 30, 'weekday', true), + (boquete_david_id, '18:30:00', 30, 'weekday', true), + (boquete_david_id, '19:00:00', 30, 'weekday', true), + (boquete_david_id, '19:30:00', 30, 'weekday', true), + (boquete_david_id, '20:00:00', 30, 'weekday', true), + + -- David > Boquete (every 30 minutes from 5:30 to 20:30) + (david_boquete_id, '05:30:00', 30, 'weekday', true), + (david_boquete_id, '06:00:00', 30, 'weekday', true), + (david_boquete_id, '06:30:00', 30, 'weekday', true), + (david_boquete_id, '07:00:00', 30, 'weekday', true), + (david_boquete_id, '07:30:00', 30, 'weekday', true), + (david_boquete_id, '08:00:00', 30, 'weekday', true), + (david_boquete_id, '08:30:00', 30, 'weekday', true), + (david_boquete_id, '09:00:00', 30, 'weekday', true), + (david_boquete_id, '09:30:00', 30, 'weekday', true), + (david_boquete_id, '10:00:00', 30, 'weekday', true), + (david_boquete_id, '10:30:00', 30, 'weekday', true), + (david_boquete_id, '11:00:00', 30, 'weekday', true), + (david_boquete_id, '11:30:00', 30, 'weekday', true), + (david_boquete_id, '12:00:00', 30, 'weekday', true), + (david_boquete_id, '12:30:00', 30, 'weekday', true), + (david_boquete_id, '13:00:00', 30, 'weekday', true), + (david_boquete_id, '13:30:00', 30, 'weekday', true), + (david_boquete_id, '14:00:00', 30, 'weekday', true), + (david_boquete_id, '14:30:00', 30, 'weekday', true), + (david_boquete_id, '15:00:00', 30, 'weekday', true), + (david_boquete_id, '15:30:00', 30, 'weekday', true), + (david_boquete_id, '16:00:00', 30, 'weekday', true), + (david_boquete_id, '16:30:00', 30, 'weekday', true), + (david_boquete_id, '17:00:00', 30, 'weekday', true), + (david_boquete_id, '17:30:00', 30, 'weekday', true), + (david_boquete_id, '18:00:00', 30, 'weekday', true), + (david_boquete_id, '18:30:00', 30, 'weekday', true), + (david_boquete_id, '19:00:00', 30, 'weekday', true), + (david_boquete_id, '19:30:00', 30, 'weekday', true), + (david_boquete_id, '20:00:00', 30, 'weekday', true), + (david_boquete_id, '20:30:00', 30, 'weekday', true), + + -- Palmira routes (every 60 minutes) + (palmira_david_id, '06:00:00', 60, 'weekday', true), + (palmira_david_id, '07:00:00', 60, 'weekday', true), + (palmira_david_id, '08:00:00', 60, 'weekday', true), + (palmira_david_id, '12:00:00', 60, 'weekday', true), + (palmira_david_id, '17:00:00', 60, 'weekday', true), + (palmira_david_id, '18:00:00', 60, 'weekday', true), + + (david_palmira_id, '08:00:00', 60, 'weekday', true), + (david_palmira_id, '14:00:00', 60, 'weekday', true), + (david_palmira_id, '18:00:00', 60, 'weekday', true), + + -- Caldera routes (every 45 minutes) + (caldera_david_id, '05:30:00', 45, 'weekday', true), + (caldera_david_id, '06:15:00', 45, 'weekday', true), + (caldera_david_id, '07:00:00', 45, 'weekday', true), + (caldera_david_id, '07:45:00', 45, 'weekday', true), + (caldera_david_id, '16:00:00', 45, 'weekday', true), + (caldera_david_id, '17:00:00', 45, 'weekday', true), + + (david_caldera_id, '09:00:00', 45, 'weekday', true), + (david_caldera_id, '15:00:00', 45, 'weekday', true), + (david_caldera_id, '18:30:00', 45, 'weekday', true); + + RAISE NOTICE 'SIBU Transportation System mock data created successfully!'; + +EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE 'Error creating mock data: %', SQLERRM; +END $$; \ No newline at end of file diff --git a/old/supabase/migrations/20241019220000_fix_existing_schema.sql b/old/supabase/migrations/20241019220000_fix_existing_schema.sql new file mode 100644 index 0000000..6feaa5f --- /dev/null +++ b/old/supabase/migrations/20241019220000_fix_existing_schema.sql @@ -0,0 +1,362 @@ +-- Fix existing SIBU transportation schema to match required structure +-- This migration updates the existing schema rather than recreating it + +-- 1. First, let's add missing columns to routes table if they don't exist +DO $$ +BEGIN + -- Add missing columns to routes table + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'description') THEN + ALTER TABLE public.routes ADD COLUMN description TEXT; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'origin_city') THEN + ALTER TABLE public.routes ADD COLUMN origin_city TEXT; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'destination_city') THEN + ALTER TABLE public.routes ADD COLUMN destination_city TEXT; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'distance_km') THEN + ALTER TABLE public.routes ADD COLUMN distance_km DECIMAL(6,2); + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'estimated_duration_minutes') THEN + ALTER TABLE public.routes ADD COLUMN estimated_duration_minutes INTEGER; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'status') THEN + -- Create enum type if it doesn't exist + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'route_status') THEN + CREATE TYPE public.route_status AS ENUM ('active', 'inactive', 'maintenance'); + END IF; + ALTER TABLE public.routes ADD COLUMN status public.route_status DEFAULT 'active'::public.route_status; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'created_at') THEN + ALTER TABLE public.routes ADD COLUMN created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'updated_at') THEN + ALTER TABLE public.routes ADD COLUMN updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP; + END IF; +END $$; + +-- 2. Add missing columns to stops table +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'city') THEN + ALTER TABLE public.stops ADD COLUMN city TEXT; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'address') THEN + ALTER TABLE public.stops ADD COLUMN address TEXT; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'stop_type') THEN + -- Create enum type if it doesn't exist + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'stop_type') THEN + CREATE TYPE public.stop_type AS ENUM ('terminal', 'regular', 'express_only'); + END IF; + ALTER TABLE public.stops ADD COLUMN stop_type public.stop_type DEFAULT 'regular'::public.stop_type; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'has_shelter') THEN + ALTER TABLE public.stops ADD COLUMN has_shelter BOOLEAN DEFAULT false; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'has_seating') THEN + ALTER TABLE public.stops ADD COLUMN has_seating BOOLEAN DEFAULT false; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'is_accessible') THEN + ALTER TABLE public.stops ADD COLUMN is_accessible BOOLEAN DEFAULT false; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'created_at') THEN + ALTER TABLE public.stops ADD COLUMN created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'updated_at') THEN + ALTER TABLE public.stops ADD COLUMN updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP; + END IF; +END $$; + +-- 3. Add missing columns to route_stops table +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'route_stops' AND column_name = 'id') THEN + ALTER TABLE public.route_stops ADD COLUMN id UUID DEFAULT gen_random_uuid(); + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'route_stops' AND column_name = 'stop_order') THEN + ALTER TABLE public.route_stops ADD COLUMN stop_order INTEGER; + -- Update stop_order from existing seq column + UPDATE public.route_stops SET stop_order = seq WHERE stop_order IS NULL; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'route_stops' AND column_name = 'travel_time_minutes') THEN + ALTER TABLE public.route_stops ADD COLUMN travel_time_minutes INTEGER; + -- Calculate travel time from dwell_sec (convert seconds to minutes) + UPDATE public.route_stops SET travel_time_minutes = COALESCE(dwell_sec / 60, 0) WHERE travel_time_minutes IS NULL; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'route_stops' AND column_name = 'is_pickup_point') THEN + ALTER TABLE public.route_stops ADD COLUMN is_pickup_point BOOLEAN DEFAULT true; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'route_stops' AND column_name = 'is_dropoff_point') THEN + ALTER TABLE public.route_stops ADD COLUMN is_dropoff_point BOOLEAN DEFAULT true; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'route_stops' AND column_name = 'created_at') THEN + ALTER TABLE public.route_stops ADD COLUMN created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP; + END IF; +END $$; + +-- 4. Create bus_schedules table based on existing timetable +CREATE TABLE IF NOT EXISTS public.bus_schedules ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + route_id TEXT REFERENCES public.routes(id) ON DELETE CASCADE, + departure_time TIME NOT NULL, + frequency_minutes INTEGER DEFAULT 30, + schedule_type TEXT DEFAULT 'weekday', + is_active BOOLEAN DEFAULT true, + notes TEXT, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +-- 5. Migrate data from timetable to bus_schedules +INSERT INTO public.bus_schedules (route_id, departure_time, frequency_minutes, schedule_type, is_active) +SELECT + route_id, + departure_time, + 30 as frequency_minutes, + 'weekday' as schedule_type, + true as is_active +FROM public.timetable +WHERE NOT EXISTS ( + SELECT 1 FROM public.bus_schedules bs + WHERE bs.route_id = timetable.route_id + AND bs.departure_time = timetable.departure_time +); + +-- 6. Update existing route data with proper values +UPDATE public.routes SET + description = CASE + WHEN name = 'Boquete – David' THEN 'Ruta desde Boquete hacia David con paradas principales' + WHEN name = 'David – Boquete' THEN 'Ruta desde David hacia Boquete con paradas principales' + ELSE 'Ruta de transporte público' + END, + origin_city = CASE + WHEN direction = 'outbound' AND name LIKE 'Boquete%' THEN 'Boquete' + WHEN direction = 'inbound' AND name LIKE 'David%' THEN 'David' + ELSE SPLIT_PART(name, ' – ', 1) + END, + destination_city = CASE + WHEN direction = 'outbound' AND name LIKE '%David' THEN 'David' + WHEN direction = 'inbound' AND name LIKE '%Boquete' THEN 'Boquete' + ELSE SPLIT_PART(name, ' – ', 2) + END, + distance_km = 38.5, + estimated_duration_minutes = 45, + status = 'active'::public.route_status +WHERE description IS NULL; + +-- 7. Add additional routes for Panama SIBU system +INSERT INTO public.routes (id, name, description, color, direction, origin_city, destination_city, distance_km, estimated_duration_minutes, status) VALUES + ('palmira-david-out', 'Palmira>David', 'Ruta desde Palmira hacia David', '#FEE715', 'outbound', 'Palmira', 'David', 25.2, 35, 'active'), + ('david-palmira-in', 'David>Palmira', 'Ruta desde David hacia Palmira', '#FEE715', 'inbound', 'David', 'Palmira', 25.2, 35, 'active'), + ('caldera-david-out', 'Caldera>David', 'Ruta desde Caldera hacia David', '#FEE715', 'outbound', 'Caldera', 'David', 42.8, 50, 'active'), + ('david-caldera-in', 'David>Caldera', 'Ruta desde David hacia Caldera', '#FEE715', 'inbound', 'David', 'Caldera', 42.8, 50, 'active') +ON CONFLICT (id) DO NOTHING; + +-- 8. Add missing bus stops for complete Panama routes +INSERT INTO public.stops (name, lat, lng, city, address, stop_type, has_shelter, has_seating) VALUES + -- Palmira stops + ('Centro de Palmira', 8.3544, -82.3611, 'Palmira', 'Plaza Central', 'regular', true, true), + ('Escuela de Palmira', 8.3567, -82.3598, 'Palmira', 'Zona Escolar', 'regular', false, true), + + -- Caldera stops + ('Puerto de Caldera', 8.2456, -81.7234, 'Caldera', 'Zona Portuaria', 'terminal', true, true), + ('Centro de Caldera', 8.2478, -81.7198, 'Caldera', 'Centro del Pueblo', 'regular', true, false), + + -- Additional David stops + ('Terminal de David', 8.4177, -82.4270, 'David', 'Terminal de Transporte', 'terminal', true, true), + ('Centro de David', 8.4194, -82.4255, 'David', 'Parque Cervantes', 'regular', true, true), + ('Hospital Chiriquí', 8.4156, -82.4289, 'David', 'Complejo Hospitalario', 'regular', true, true), + ('Chiriquí Mall', 8.4089, -82.4178, 'David', 'Centro Comercial', 'regular', true, true), + + -- Additional Boquete stops + ('Terminal de Boquete', 8.7697, -82.4328, 'Boquete', 'Centro de Boquete', 'terminal', true, true), + ('Centro de Boquete', 8.7720, -82.4315, 'Boquete', 'Calle Central', 'regular', true, true), + ('Parque Central Boquete', 8.7705, -82.4340, 'Boquete', 'Junto al Parque Central', 'regular', false, true), + ('Hospital de Boquete', 8.7680, -82.4350, 'Boquete', 'Hospital Regional', 'regular', true, true), + ('Escuela Primaria Boquete', 8.7740, -82.4300, 'Boquete', 'Zona Educativa', 'regular', false, false), + ('Mercado Municipal Boquete', 8.7715, -82.4365, 'Boquete', 'Mercado Central', 'regular', true, true) +ON CONFLICT (name, lat, lng) DO NOTHING; + +-- 9. Helper Functions +CREATE OR REPLACE FUNCTION public.calculate_next_bus_arrival( + p_route_id TEXT, + p_stop_id UUID, + p_current_time TIME DEFAULT CURRENT_TIME +) +RETURNS TABLE( + next_departure TIME, + estimated_arrival_time TIMESTAMPTZ, + minutes_until_arrival INTEGER +) +LANGUAGE plpgsql +STABLE +SECURITY DEFINER +AS $$ +DECLARE + stop_position INTEGER; + time_to_stop INTEGER; + next_schedule_time TIME; +BEGIN + -- Get the next scheduled departure + SELECT bs.departure_time INTO next_schedule_time + FROM public.bus_schedules bs + WHERE bs.route_id = p_route_id + AND bs.is_active = true + AND bs.departure_time > p_current_time + ORDER BY bs.departure_time ASC + LIMIT 1; + + -- If no more schedules today, get first schedule tomorrow + IF next_schedule_time IS NULL THEN + SELECT bs.departure_time INTO next_schedule_time + FROM public.bus_schedules bs + WHERE bs.route_id = p_route_id + AND bs.is_active = true + ORDER BY bs.departure_time ASC + LIMIT 1; + END IF; + + -- Get stop position in route and calculate travel time + SELECT rs.stop_order INTO stop_position + FROM public.route_stops rs + WHERE rs.route_id = p_route_id AND rs.stop_id = p_stop_id; + + -- Calculate estimated travel time to this stop (simplified calculation) + time_to_stop := COALESCE((stop_position - 1) * 2 + (stop_position * 3), 5); -- ~3 min between stops + + RETURN QUERY SELECT + next_schedule_time, + (CURRENT_DATE + next_schedule_time + (time_to_stop || ' minutes')::INTERVAL)::TIMESTAMPTZ, + EXTRACT(EPOCH FROM ( + (CURRENT_DATE + next_schedule_time + (time_to_stop || ' minutes')::INTERVAL) - NOW() + ))::INTEGER / 60; +END; +$$; + +CREATE OR REPLACE FUNCTION public.get_route_stops_ordered(p_route_id TEXT) +RETURNS TABLE( + stop_id UUID, + stop_name TEXT, + latitude DOUBLE PRECISION, + longitude DOUBLE PRECISION, + stop_order INTEGER, + travel_time_minutes INTEGER +) +LANGUAGE sql +STABLE +SECURITY DEFINER +AS $$ +SELECT + s.id, + s.name, + s.lat, + s.lng, + rs.stop_order, + rs.travel_time_minutes +FROM public.route_stops rs +JOIN public.stops s ON rs.stop_id = s.id +WHERE rs.route_id = p_route_id +ORDER BY rs.stop_order; +$$; + +-- 10. Enable RLS if not already enabled +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'routes' AND rowsecurity = true) THEN + ALTER TABLE public.routes ENABLE ROW LEVEL SECURITY; + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stops' AND rowsecurity = true) THEN + ALTER TABLE public.stops ENABLE ROW LEVEL SECURITY; + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'route_stops' AND rowsecurity = true) THEN + ALTER TABLE public.route_stops ENABLE ROW LEVEL SECURITY; + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'bus_schedules' AND rowsecurity = true) THEN + ALTER TABLE public.bus_schedules ENABLE ROW LEVEL SECURITY; + END IF; +END $$; + +-- 11. Create RLS Policies (only if they don't exist) +DO $$ +BEGIN + -- Routes policies + IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'routes' AND policyname = 'public_can_read_routes') THEN + CREATE POLICY "public_can_read_routes" ON public.routes FOR SELECT TO public USING (true); + END IF; + + -- Stops policies + IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'stops' AND policyname = 'public_can_read_stops') THEN + CREATE POLICY "public_can_read_stops" ON public.stops FOR SELECT TO public USING (true); + END IF; + + -- Route stops policies + IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'route_stops' AND policyname = 'public_can_read_route_stops') THEN + CREATE POLICY "public_can_read_route_stops" ON public.route_stops FOR SELECT TO public USING (true); + END IF; + + -- Bus schedules policies + IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'bus_schedules' AND policyname = 'public_can_read_bus_schedules') THEN + CREATE POLICY "public_can_read_bus_schedules" ON public.bus_schedules FOR SELECT TO public USING (true); + END IF; +END $$; + +-- 12. Create updated_at triggers +CREATE OR REPLACE FUNCTION public.update_updated_at_column() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_routes_updated_at') THEN + CREATE TRIGGER update_routes_updated_at + BEFORE UPDATE ON public.routes + FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_stops_updated_at') THEN + CREATE TRIGGER update_stops_updated_at + BEFORE UPDATE ON public.stops + FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + END IF; +END $$; + +-- 13. Create missing indexes for performance +CREATE INDEX IF NOT EXISTS idx_routes_origin_destination ON public.routes(origin_city, destination_city); +CREATE INDEX IF NOT EXISTS idx_routes_status ON public.routes(status); +CREATE INDEX IF NOT EXISTS idx_stops_city ON public.stops(city); +CREATE INDEX IF NOT EXISTS idx_bus_schedules_route_id ON public.bus_schedules(route_id); +CREATE INDEX IF NOT EXISTS idx_bus_schedules_departure_time ON public.bus_schedules(departure_time); + +-- 14. Final success message (wrapped in DO block to avoid syntax error) +DO $$ +BEGIN + RAISE NOTICE 'SIBU Transportation System schema updated successfully!'; +END $$; \ No newline at end of file diff --git a/old/supabase/migrations/20250126225029_taxi_module.sql b/old/supabase/migrations/20250126225029_taxi_module.sql new file mode 100644 index 0000000..2fd0e62 --- /dev/null +++ b/old/supabase/migrations/20250126225029_taxi_module.sql @@ -0,0 +1,77 @@ +-- Location: supabase/migrations/20250126225029_taxi_module.sql +-- Schema Analysis: Existing transportation system with routes, stops, timetable +-- Integration Type: NEW_MODULE - Adding taxi directory functionality +-- Dependencies: No direct references to existing tables (standalone module) + +-- Create tables for taxi directory functionality +CREATE TABLE public.taxis ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + phone TEXT NOT NULL, + district TEXT NOT NULL, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +-- Create favorites table (conditional user relationship) +CREATE TABLE public.favorite_taxis ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL, + taxi_id UUID REFERENCES public.taxis(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +-- Essential indexes for performance +CREATE INDEX idx_taxis_district ON public.taxis(district); +CREATE INDEX idx_taxis_is_active ON public.taxis(is_active); +CREATE INDEX idx_taxis_name ON public.taxis(name); +CREATE INDEX idx_favorite_taxis_user_id ON public.favorite_taxis(user_id); +CREATE INDEX idx_favorite_taxis_taxi_id ON public.favorite_taxis(taxi_id); + +-- Unique constraint for user-taxi favorites +CREATE UNIQUE INDEX idx_favorite_taxis_unique ON public.favorite_taxis(user_id, taxi_id); + +-- Enable RLS for security +ALTER TABLE public.taxis ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.favorite_taxis ENABLE ROW LEVEL SECURITY; + +-- RLS Policies using Pattern 4: Public Read, Private Write +CREATE POLICY "public_can_read_taxis" +ON public.taxis +FOR SELECT +TO public +USING (true); + +-- Pattern 2: Simple User Ownership for favorites +CREATE POLICY "users_manage_own_favorite_taxis" +ON public.favorite_taxis +FOR ALL +TO authenticated +USING (user_id = auth.uid()) +WITH CHECK (user_id = auth.uid()); + +-- Allow anonymous users to read favorites (for local storage fallback) +CREATE POLICY "public_can_read_favorite_taxis" +ON public.favorite_taxis +FOR SELECT +TO public +USING (true); + +-- Sample taxi data for different districts +DO $$ +BEGIN + INSERT INTO public.taxis (name, phone, district, is_active) VALUES + ('Taxi Central Boquete', '+507 720-1234', 'Boquete', true), + ('Taxi Flores David', '+507 775-5678', 'David', true), + ('Taxi Montaña Verde', '+507 720-9101', 'Boquete', true), + ('Taxi Ciudad David', '+507 775-1122', 'David', true), + ('Taxi Volcán Express', '+507 771-3344', 'Volcán', true), + ('Taxi Dolega Rápido', '+507 721-5566', 'Dolega', true), + ('Taxi Bugaba Seguro', '+507 772-7788', 'Bugaba', true), + ('Taxi Renacimiento', '+507 773-9900', 'Renacimiento', true), + ('Taxi Alanje Centro', '+507 774-2233', 'Alanje', true), + ('Taxi Boquerón', '+507 775-4455', 'Boquerón', true), + ('Taxi Los Naranjos', '+507 720-6677', 'Boquete', true), + ('Taxi San Lorenzo', '+507 775-8899', 'David', true); +END $$; \ No newline at end of file diff --git a/old/supabase/migrations/20251031165808_enhance_taxi_directory.sql b/old/supabase/migrations/20251031165808_enhance_taxi_directory.sql new file mode 100644 index 0000000..1bb3a0f --- /dev/null +++ b/old/supabase/migrations/20251031165808_enhance_taxi_directory.sql @@ -0,0 +1,97 @@ +-- Location: supabase/migrations/20251031165808_enhance_taxi_directory.sql +-- Schema Analysis: Existing taxis table with district column, needs shift column and terminology update +-- Integration Type: PARTIAL_EXISTS - Extending existing taxi module +-- Dependencies: Existing taxis and favorite_taxis tables + +-- 1. Create shift enum type for taxi shifts +CREATE TYPE public.taxi_shift AS ENUM ('day', 'evening', 'night'); + +-- 2. Add shift column to existing taxis table +ALTER TABLE public.taxis +ADD COLUMN shift public.taxi_shift DEFAULT 'day'::public.taxi_shift; + +-- 3. Rename district column to corregimiento for terminology consistency +ALTER TABLE public.taxis +RENAME COLUMN district TO corregimiento; + +-- 4. Update existing indexes to match new column names +DROP INDEX IF EXISTS idx_taxis_district; +CREATE INDEX idx_taxis_corregimiento ON public.taxis(corregimiento); +CREATE INDEX idx_taxis_shift ON public.taxis(shift); + +-- 5. Add updated sample data with new structure +DO $$ +DECLARE + taxi1_id UUID := gen_random_uuid(); + taxi2_id UUID := gen_random_uuid(); + taxi3_id UUID := gen_random_uuid(); + taxi4_id UUID := gen_random_uuid(); + taxi5_id UUID := gen_random_uuid(); + taxi6_id UUID := gen_random_uuid(); +BEGIN + -- Remove existing sample data first + DELETE FROM public.taxis WHERE phone IN ('+507 720-1234', '+507 775-5678'); + + -- Insert comprehensive sample data with corregimiento and shift + INSERT INTO public.taxis (id, name, phone, corregimiento, shift, is_active, created_at, updated_at) VALUES + (taxi1_id, 'Taxi Central Boquete', '+507 720-1234', 'Boquete', 'day'::public.taxi_shift, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + (taxi2_id, 'Taxi Flores David', '+507 775-5678', 'David', 'evening'::public.taxi_shift, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + (taxi3_id, 'Taxi Noctuno David', '+507 776-9999', 'David', 'night'::public.taxi_shift, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + (taxi4_id, 'Taxi Diurno Chiriquí', '+507 721-4567', 'Chiriquí', 'day'::public.taxi_shift, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + (taxi5_id, 'Taxi Tarde Boquete', '+507 722-8890', 'Boquete', 'evening'::public.taxi_shift, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + (taxi6_id, 'Taxi Madrugada Chiriquí', '+507 723-1122', 'Chiriquí', 'night'::public.taxi_shift, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +EXCEPTION + WHEN unique_violation THEN + RAISE NOTICE 'Some taxi data already exists, skipping duplicates'; + WHEN OTHERS THEN + RAISE NOTICE 'Error inserting taxi data: %', SQLERRM; +END $$; + +-- 6. Create helper function for getting distinct corregimientos +CREATE OR REPLACE FUNCTION public.get_distinct_corregimientos() +RETURNS TABLE(corregimiento TEXT) +LANGUAGE sql +STABLE +SECURITY DEFINER +AS $$ + SELECT DISTINCT t.corregimiento::TEXT + FROM public.taxis t + WHERE t.is_active = true + ORDER BY t.corregimiento::TEXT; +$$; + +-- 7. Create helper function for filtering taxis by corregimiento and shift +CREATE OR REPLACE FUNCTION public.filter_taxis_by_criteria( + selected_corregimiento TEXT DEFAULT NULL, + selected_shift TEXT DEFAULT NULL +) +RETURNS TABLE( + id UUID, + name TEXT, + phone TEXT, + corregimiento TEXT, + shift TEXT, + is_active BOOLEAN, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ +) +LANGUAGE sql +STABLE +SECURITY DEFINER +AS $$ + SELECT + t.id, + t.name, + t.phone, + t.corregimiento::TEXT, + t.shift::TEXT, + t.is_active, + t.created_at, + t.updated_at + FROM public.taxis t + WHERE t.is_active = true + AND (selected_corregimiento IS NULL OR t.corregimiento = selected_corregimiento) + AND (selected_shift IS NULL OR t.shift::TEXT = selected_shift) + ORDER BY t.name ASC; +$$; \ No newline at end of file diff --git a/old/supabase/migrations/20251031181019_enhance_coupons_with_categories.sql b/old/supabase/migrations/20251031181019_enhance_coupons_with_categories.sql new file mode 100644 index 0000000..591677d --- /dev/null +++ b/old/supabase/migrations/20251031181019_enhance_coupons_with_categories.sql @@ -0,0 +1,119 @@ +-- Schema Analysis: Existing coupons table with basic structure +-- Integration Type: extension - adding missing columns for enhanced functionality +-- Dependencies: existing coupons table + +-- Add missing columns to existing coupons table +ALTER TABLE public.coupons +ADD COLUMN image_url TEXT, +ADD COLUMN category TEXT, +ADD COLUMN is_active BOOLEAN DEFAULT true, +ADD COLUMN created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP; + +-- Create enum for category validation +CREATE TYPE public.coupon_category AS ENUM ( + 'restaurantes', + 'tiendas', + 'servicios', + 'entretenimiento', + 'salud', + 'belleza' +); + +-- Update category column to use enum with proper constraint +ALTER TABLE public.coupons +ALTER COLUMN category TYPE public.coupon_category USING category::public.coupon_category; + +-- Add indexes for performance +CREATE INDEX idx_coupons_category ON public.coupons(category); +CREATE INDEX idx_coupons_is_active ON public.coupons(is_active); +CREATE INDEX idx_coupons_created_at ON public.coupons(created_at); +CREATE INDEX idx_coupons_valid_until ON public.coupons(valid_until); + +-- Enable RLS if not already enabled +ALTER TABLE public.coupons ENABLE ROW LEVEL SECURITY; + +-- RLS Policy: Public read access for active and valid coupons +CREATE POLICY "public_can_read_active_coupons" +ON public.coupons +FOR SELECT +TO public +USING ( + is_active = true + AND (valid_until IS NULL OR valid_until >= CURRENT_DATE) +); + +-- Mock data with proper Spanish categories +DO $$ +BEGIN + -- Insert sample coupons with all required fields + INSERT INTO public.coupons ( + business_name, title, description, valid_until, + image_url, category, is_active, created_at + ) VALUES + ( + 'Restaurante El Buen Sabor', + 'Descuento del 20%', + 'Descuento válido en toda la carta. No aplica con otras promociones.', + '2025-01-15'::date, + 'https://images.unsplash.com/photo-1647695822638-a40e238ddc39', + 'restaurantes'::public.coupon_category, + true, + '2024-10-15 10:00:00'::timestamptz + ), + ( + 'Tienda La Moderna', + 'Descuento de $10', + 'Descuento en compras mayores a $50. Válido en toda la tienda.', + '2024-12-31'::date, + 'https://images.unsplash.com/photo-1599190118801-8e4463c6a993', + 'tiendas'::public.coupon_category, + true, + '2024-10-10 14:30:00'::timestamptz + ), + ( + 'Spa Relajación Total', + 'Descuento del 30% en masajes', + 'Descuento en todos los tipos de masajes. Incluye aromaterapia.', + '2024-11-30'::date, + 'https://images.unsplash.com/photo-1706795033849-7ca391f007c5', + 'belleza'::public.coupon_category, + true, + '2024-10-05 09:15:00'::timestamptz + ), + ( + 'Café Montaña', + 'Café gratis con postre', + 'Café americano gratis con la compra de cualquier postre.', + '2024-10-25'::date, + 'https://images.unsplash.com/photo-1587299103717-ca5b2db07f47', + 'restaurantes'::public.coupon_category, + true, + '2024-10-12 11:20:00'::timestamptz + ), + ( + 'Farmacia San José', + 'Descuento del 15% en medicamentos', + 'Descuento en medicamentos de venta libre y productos de cuidado personal.', + '2024-12-15'::date, + 'https://images.unsplash.com/photo-1701117553039-d39c1ee3f3cd', + 'salud'::public.coupon_category, + true, + '2024-10-08 13:45:00'::timestamptz + ), + ( + 'Cine Boquete', + '2x1 en entradas', + 'Dos entradas por el precio de una. Válido de lunes a miércoles.', + '2024-11-20'::date, + 'https://images.unsplash.com/photo-1669697243629-8c8a5d6dad9e', + 'entretenimiento'::public.coupon_category, + true, + '2024-10-01 08:30:00'::timestamptz + ); + +EXCEPTION + WHEN duplicate_object THEN + RAISE NOTICE 'Objects already exist, skipping creation'; + WHEN OTHERS THEN + RAISE NOTICE 'Migration error: %', SQLERRM; +END $$; \ No newline at end of file diff --git a/old/test/widget_test.dart b/old/test/widget_test.dart new file mode 100644 index 0000000..82bb21c --- /dev/null +++ b/old/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:sibu/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +}