diff --git a/transitrack_web/.firebaserc b/transitrack_web/.firebaserc index 6ab130d..a3c8a31 100644 --- a/transitrack_web/.firebaserc +++ b/transitrack_web/.firebaserc @@ -1,5 +1,8 @@ { "projects": { - "default": "jeeps-v2" - } + "default": "jeeps-v2", + "jeeps-v2": "jeeps-v2" + }, + "targets": {}, + "etags": {} } diff --git a/transitrack_web/cors.json b/transitrack_web/cors.json new file mode 100644 index 0000000..920a885 --- /dev/null +++ b/transitrack_web/cors.json @@ -0,0 +1,17 @@ +[ + { + "origin": [ + "*" + ], + "method": [ + "GET", + "POST" + ], + "responseHeader": [ + "Content-Type", + "Access-Control-Allow-Origin", + "Access-Control-Allow-Headers" + ], + "maxAgeSeconds": 7200 + } +] \ No newline at end of file diff --git a/transitrack_web/devtools_options.yaml b/transitrack_web/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/transitrack_web/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/transitrack_web/firebase.json b/transitrack_web/firebase.json index 8976dc8..842b4f8 100644 --- a/transitrack_web/firebase.json +++ b/transitrack_web/firebase.json @@ -1,12 +1,32 @@ { "hosting": { "public": "build/web", - "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], "rewrites": [ { "source": "**", "destination": "/index.html" } ] - } + }, + "functions": [ + { + "source": "functions", + "codebase": "default", + "ignore": [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log", + "*.local" + ], + "predeploy": [ + "npm --prefix \"$RESOURCE_DIR\" run build" + ] + } + ] } diff --git a/transitrack_web/functions/.gitignore b/transitrack_web/functions/.gitignore new file mode 100644 index 0000000..9be0f01 --- /dev/null +++ b/transitrack_web/functions/.gitignore @@ -0,0 +1,10 @@ +# Compiled JavaScript files +lib/**/*.js +lib/**/*.js.map + +# TypeScript v1 declaration files +typings/ + +# Node.js dependency directory +node_modules/ +*.local \ No newline at end of file diff --git a/transitrack_web/functions/package-lock.json b/transitrack_web/functions/package-lock.json new file mode 100644 index 0000000..5cf2fea --- /dev/null +++ b/transitrack_web/functions/package-lock.json @@ -0,0 +1,6391 @@ +{ + "name": "functions", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "functions", + "dependencies": { + "firebase-admin": "^12.7.0", + "firebase-functions": "^6.3.2" + }, + "devDependencies": { + "firebase-functions-test": "^3.1.0", + "typescript": "^4.9.0" + }, + "engines": { + "node": "22" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", + "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@babel/types": "^7.26.9", + "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/core/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@babel/generator": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", + "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", + "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/types": "^7.26.9" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "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", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", + "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@babel/types": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", + "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@fastify/busboy": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.1.tgz", + "integrity": "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==", + "license": "MIT" + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz", + "integrity": "sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-types": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.2.tgz", + "integrity": "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz", + "integrity": "sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/component": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.9.tgz", + "integrity": "sha512-gm8EUEJE/fEac86AvHn8Z/QW8BvR56TBw3hMW0O838J/1mThYQXAIQBgUv75EqlCZfdawpWLrKt1uXvp9ciK3Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.10.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.8.tgz", + "integrity": "sha512-dzXALZeBI1U5TXt6619cv0+tgEhJiwlUtQ55WNZY7vGAjv7Q1QioV969iYwt1AQQ0ovHnEW0YW9TiBfefLvErg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.2", + "@firebase/auth-interop-types": "0.2.3", + "@firebase/component": "0.6.9", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.10.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.8.tgz", + "integrity": "sha512-OpeWZoPE3sGIRPBKYnW9wLad25RaWbGyk7fFQe4xnJQKRzlynWeFBSRRAoLE2Old01WXwskUiucNqUUVlFsceg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.9", + "@firebase/database": "1.0.8", + "@firebase/database-types": "1.0.5", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.10.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.5.tgz", + "integrity": "sha512-fTlqCNwFYyq/C6W7AJ5OCuq5CeZuBEsEwptnVxlNPkWCo5cTTyukzAHRSO/jaQcItz33FfYrrFk1SJofcu2AaQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.2", + "@firebase/util": "1.10.0" + } + }, + "node_modules/@firebase/logger": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.2.tgz", + "integrity": "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.10.0.tgz", + "integrity": "sha512-xKtx4A668icQqoANRxyDLBLz51TAbDP9KRfpbKGxiCAW346d0BeJe5vN6/hKxxmWwnZ0mautyv39JxviwwQMOQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.0.tgz", + "integrity": "sha512-88uZ+jLsp1aVMj7gh3EKYH1aulTAMFAp8sH/v5a9w8q8iqSG27RiWLoxSAFr/XocZ9hGiWH1kEnBw+zl3xAgNA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/api": "^1.3.0", + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^4.3.3", + "protobufjs": "^7.2.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage": { + "version": "7.15.2", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.15.2.tgz", + "integrity": "sha512-+2k+mcQBb9zkaXMllf2wwR/rI07guAx+eZLWsGTDihW2lJRGfiqB7xu1r7/s4uvSP/T+nAumvzT5TTscwHKJ9A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "^4.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^4.4.1", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.12.6", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.6.tgz", + "integrity": "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.8.tgz", + "integrity": "sha512-7fx54m60nLFUVYlxAB1xpe9CBWX2vSrk50Y6ogRJ1v5xxtba7qXTg5BgYDN5dq+yuQQ9HaVlHJyAAt1/mxryFg==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", + "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "optional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "license": "MIT", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT", + "optional": true + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.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", + "peer": true + }, + "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", + "optional": true + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "license": "MIT", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "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": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "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", + "peer": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001700", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz", + "integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==", + "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", + "peer": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "optional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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", + "peer": true + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "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", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "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/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.102", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.102.tgz", + "integrity": "sha512-eHhqaja8tE/FNpIiBrvBjFV/SSKpyWHLvxuR9dPTdo+3V9ppdLmFB7ZZQ98qNovcngPLYIz0oOBF9P0FfZef5Q==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "optional": true, + "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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT", + "optional": true + }, + "node_modules/farmhash-modern": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/farmhash-modern/-/farmhash-modern-1.1.0.tgz", + "integrity": "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==", + "license": "MIT", + "engines": { + "node": ">=18.0.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==", + "license": "MIT", + "optional": true + }, + "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", + "peer": true + }, + "node_modules/fast-xml-parser": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "strnum": "^1.1.1" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-admin": { + "version": "12.7.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.7.0.tgz", + "integrity": "sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA==", + "license": "Apache-2.0", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@firebase/database-compat": "1.0.8", + "@firebase/database-types": "1.0.5", + "@types/node": "^22.0.1", + "farmhash-modern": "^1.1.0", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.1.0", + "node-forge": "^1.3.1", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^7.7.0", + "@google-cloud/storage": "^7.7.0" + } + }, + "node_modules/firebase-functions": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-6.3.2.tgz", + "integrity": "sha512-FC3A1/nhqt1ZzxRnj5HZLScQaozAcFSD/vSR8khqSoFNOfxuXgwJS6ZABTB7+v+iMD5z6Mmxw6OfqITUBuI7OQ==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "^4.17.21", + "cors": "^2.8.5", + "express": "^4.21.0", + "protobufjs": "^7.2.2" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": ">=14.10.0" + }, + "peerDependencies": { + "firebase-admin": "^11.10.0 || ^12.0.0 || ^13.0.0" + } + }, + "node_modules/firebase-functions-test": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/firebase-functions-test/-/firebase-functions-test-3.4.0.tgz", + "integrity": "sha512-ignkiegIvGtCbDZFEKerLrzrKGonCHD/VJsuNhcfz3jDfhP9yN7mJeq7AHXz8cOJaAnBnJ0WxHj3xezem2dEbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "^4.14.104", + "lodash": "^4.17.5", + "ts-deepmerge": "^2.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "firebase-admin": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0", + "firebase-functions": ">=4.9.0", + "jest": ">=28.0.0" + } + }, + "node_modules/form-data": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz", + "integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "license": "MIT", + "optional": true + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "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", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "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-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.4.1.tgz", + "integrity": "sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "^1.10.9", + "@grpc/proto-loader": "^0.7.13", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.7.0", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.2", + "protobufjs": "^7.3.2", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "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==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "optional": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", + "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==", + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/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", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/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", + "optional": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "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/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "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", + "peer": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.12.0" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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", + "peer": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "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", + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/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/jsonwebtoken/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "license": "MIT", + "dependencies": { + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.2", + "debug": "^4.3.4", + "jose": "^4.14.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/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/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "optional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "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", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", + "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==", + "license": "Apache-2.0" + }, + "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", + "peer": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer/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/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "peer": true, + "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", + "peer": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC", + "peer": true + }, + "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", + "peer": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proto3-json-serializer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "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", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/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/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "peer": true, + "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==", + "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==", + "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==", + "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==", + "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": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "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", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT", + "optional": true + }, + "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", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "devOptional": 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/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.1.tgz", + "integrity": "sha512-O7aCHfYCamLCctjAiaucmE+fHf2DYHkus2OKCn4Wv03sykfFtgeECn505X6K4mPl8CRNd/qurC9guq+ynoN4pw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT", + "optional": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/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", + "optional": true + }, + "node_modules/teeny-request/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT", + "optional": true + }, + "node_modules/ts-deepmerge": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-2.0.7.tgz", + "integrity": "sha512-3phiGcxPSSR47RBubQxPoZ+pqXsEsozLo4G4AlSrsMKTFg9TA3l+3he5BqpUi9wiuDbaHWXH/amlzQ49uEdXtg==", + "dev": true, + "license": "ISC" + }, + "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-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "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": { + "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", + "optional": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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==", + "devOptional": 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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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", + "peer": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/transitrack_web/functions/package.json b/transitrack_web/functions/package.json new file mode 100644 index 0000000..42c9db5 --- /dev/null +++ b/transitrack_web/functions/package.json @@ -0,0 +1,25 @@ +{ + "name": "functions", + "scripts": { + "build": "tsc", + "build:watch": "tsc --watch", + "serve": "npm run build && firebase emulators:start --only functions", + "shell": "npm run build && firebase functions:shell", + "start": "npm run shell", + "deploy": "firebase deploy --only functions", + "logs": "firebase functions:log" + }, + "engines": { + "node": "22" + }, + "main": "lib/index.js", + "dependencies": { + "firebase-admin": "^12.7.0", + "firebase-functions": "^6.3.2" + }, + "devDependencies": { + "firebase-functions-test": "^3.1.0", + "typescript": "^4.9.0" + }, + "private": true +} diff --git a/transitrack_web/functions/src/index.ts b/transitrack_web/functions/src/index.ts new file mode 100644 index 0000000..1e5ed07 --- /dev/null +++ b/transitrack_web/functions/src/index.ts @@ -0,0 +1,56 @@ +import * as functions from "firebase-functions/v1"; +import * as admin from "firebase-admin"; + +admin.initializeApp(); + +export const toggleUserStatus = functions.https.onCall( + async ( + data: { uid: string; disable: boolean }, + context: functions.https.CallableContext + ) => { + // Check if the request is authenticated + if (!context?.auth) { + throw new functions.https.HttpsError( + "unauthenticated", + "The function must be called while authenticated." + ); + } + + const { uid, disable } = data; // Get the user ID and disable flag from request data + + if (!uid || typeof disable !== "boolean") { + throw new functions.https.HttpsError( + "invalid-argument", + "The function must be called with a valid user ID and disable flag." + ); + } + + try { + // Update authentication status + await admin.auth().updateUser(uid, { disabled: disable }); + + // Update Firestore account status only for the document where account_uid matches uid + const accountsRef = admin.firestore().collection("accounts"); + const querySnapshot = await accountsRef + .where("account_uid", "==", uid) + .get(); + + if (!querySnapshot.empty) { + querySnapshot.forEach(async (doc) => { + await doc.ref.set({ account_banned: disable }, { merge: true }); + }); + } + + return { + message: `Successfully ${ + disable ? "disabled" : "enabled" + } user with UID: ${uid}`, + }; + } catch (error) { + throw new functions.https.HttpsError( + "internal", + `Error updating user status: ${(error as Error).message}` + ); + } + } +); diff --git a/transitrack_web/functions/tsconfig.json b/transitrack_web/functions/tsconfig.json new file mode 100644 index 0000000..57b915f --- /dev/null +++ b/transitrack_web/functions/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "esModuleInterop": true, + "moduleResolution": "nodenext", + "noImplicitReturns": true, + "noUnusedLocals": true, + "outDir": "lib", + "sourceMap": true, + "strict": true, + "target": "es2017" + }, + "compileOnSave": true, + "include": [ + "src" + ] +} diff --git a/transitrack_web/lib/components/account_related/login_form.dart b/transitrack_web/lib/components/account_related/login_form.dart index ffee193..2bf3d5d 100644 --- a/transitrack_web/lib/components/account_related/login_form.dart +++ b/transitrack_web/lib/components/account_related/login_form.dart @@ -43,7 +43,14 @@ class _LoginFormState extends State { } on FirebaseAuthException catch (e) { // pop loading circle Navigator.pop(context); - errorMessage(e.code); + + // check if the error is due to a disabled user + if (e.code == 'user-disabled') { + errorMessage( + "Your account has been banned. Please contact rpdecena@up.edu.ph."); + } else { + errorMessage(e.code); + } } } diff --git a/transitrack_web/lib/components/account_related/route_manager/data_visualization.dart b/transitrack_web/lib/components/account_related/route_manager/data_visualization.dart index 201b779..a55369f 100644 --- a/transitrack_web/lib/components/account_related/route_manager/data_visualization.dart +++ b/transitrack_web/lib/components/account_related/route_manager/data_visualization.dart @@ -3,6 +3,7 @@ import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:transitrack_web/components/account_related/route_manager/data_visualization/feedbacks_table.dart'; import 'package:transitrack_web/components/account_related/route_manager/data_visualization/jeep_historical.dart'; import 'package:transitrack_web/components/account_related/route_manager/data_visualization/manage_drivers_table.dart'; +import 'package:transitrack_web/components/account_related/route_manager/data_visualization/manage_commuters_table.dart'; import 'package:transitrack_web/components/account_related/route_manager/data_visualization/reports_table.dart'; import 'package:transitrack_web/components/account_related/route_manager/data_visualization/shared_locations_page.dart'; import 'package:transitrack_web/components/left_drawer/logo.dart'; @@ -52,10 +53,15 @@ class _DataVisualizationTabState extends State { routeData: widget.route, )), DataVisualizationMenuList( - menuName: "Manage Drivers", + menuName: "Drivers", menuWidget: ManageDriversTable( route: widget.route, )), + DataVisualizationMenuList( + menuName: "Commuters", + menuWidget: ManageCommutersTable( + route: widget.route, + )), ]; int selected = -1; @@ -78,13 +84,23 @@ class _DataVisualizationTabState extends State { mainAxisAlignment: MainAxisAlignment.start, children: [ Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, // Space between text and button children: [ PrimaryText( text: widget.route.routeName, color: Colors.white, size: 40, fontWeight: FontWeight.w700, - ) + ), + IconButton( + icon: + const Icon(Icons.close, color: Colors.white), // Close icon + onPressed: () { + Navigator.pop(context); // Close the current screen + }, + tooltip: "Close", // Tooltip for accessibility + ), ], ), const Divider(color: Colors.white), @@ -121,7 +137,7 @@ class _DataVisualizationTabState extends State { Expanded( child: selected == -1 ? const SizedBox( - height: 700, + height: 500, child: Center( child: Logo(), )) diff --git a/transitrack_web/lib/components/account_related/route_manager/data_visualization/feedbacks_table.dart b/transitrack_web/lib/components/account_related/route_manager/data_visualization/feedbacks_table.dart index c66f3f8..b941d85 100644 --- a/transitrack_web/lib/components/account_related/route_manager/data_visualization/feedbacks_table.dart +++ b/transitrack_web/lib/components/account_related/route_manager/data_visualization/feedbacks_table.dart @@ -70,6 +70,34 @@ class _FeedbacksTableState extends State { }); } + Future deleteFeedback(String senderEmail, Timestamp timestamp) async { + try { + // Query the Firestore collection to find feedback documents + QuerySnapshot querySnapshot = await FirebaseFirestore.instance + .collection('feedbacks') + .where('feedback_sender', + isEqualTo: senderEmail) // Filter by sender email + .where('timestamp', isEqualTo: timestamp) // Filter by timestamp + .limit(1) // Limit the query to 1 document + .get(); // Retrieve the matching document + + // Check if any document was found. + if (querySnapshot.docs.isNotEmpty) { + // Get the first document and delete it. + await querySnapshot.docs.first.reference.delete(); + + // Print success message in the console. + // print("Feedback deleted successfully."); + } else { + // Print a message if no matching feedback is found. + // print("No matching feedback found to delete."); + } + } catch (error) { + // Print an error message if something goes wrong. + // print("Error deleting feedback: $error"); + } + } + @override Widget build(BuildContext context) { return Padding( @@ -77,7 +105,7 @@ class _FeedbacksTableState extends State { child: Row( children: [ SizedBox( - height: 700, + height: 500, width: 500, child: Column( children: [ @@ -138,7 +166,7 @@ class _FeedbacksTableState extends State { ), if (feedbacks == null) SizedBox( - height: 500, + height: 300, child: Center( child: CircularProgressIndicator( color: Color(widget.route.routeColor), @@ -225,243 +253,325 @@ class _FeedbacksTableState extends State { snapshot.data!; return SizedBox( - height: 700, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Center( - child: Container( - width: 34, - height: 34, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Color(widget.route - .routeColor), // Circle color - ), - child: const Center( - child: Icon( - Icons.person, - size: 22, - color: Constants - .bgColor, // Icon color + height: 500, + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Center( + child: Container( + width: 34, + height: 34, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Color(widget.route + .routeColor), // Circle color + ), + child: const Center( + child: Icon( + Icons.person, + size: 22, + color: Constants + .bgColor, // Icon color + ), ), ), ), - ), - const SizedBox( - width: Constants.defaultPadding), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - 'Feedback by ${feedbackAdditionalInfo.senderData != null ? feedbackAdditionalInfo.senderData!.account_name : "No Data"}'), - Text( - '<${selectedFeedback!.feedback_sender}>', - style: TextStyle( - fontSize: 11, - color: Colors.white - .withValues( - alpha: 0.5))), - ], - ), - ], - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.end, - children: [ - Text(DateFormat('MMMM d, y').format( - selectedFeedback!.timestamp - .toDate())), - Text( - DateFormat('hh:mm a').format( - selectedFeedback!.timestamp - .toDate()), - style: TextStyle( - fontSize: 13, - color: Colors.white - .withValues(alpha: 0.5))), - ], - ) - ], - ), - const Spacer(), - Center( - child: Container( - width: 500, - padding: const EdgeInsets.all( - Constants.defaultPadding * 2), - decoration: BoxDecoration( - border: Border.all( - width: 2, - color: Colors.white - .withValues(alpha: 0.5)), - borderRadius: BorderRadius.circular( - Constants.defaultPadding / 2)), - child: Column( - children: [ - Row( - children: [ - Expanded( - child: Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, + const SizedBox( + width: Constants.defaultPadding), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text(feedbackAdditionalInfo + .senderData != + null + ? feedbackAdditionalInfo + .senderData!.account_name + : "No Data"), + Text( + selectedFeedback! + .feedback_sender, + style: TextStyle( + fontSize: 11, + color: Colors.white + .withValues( + alpha: 0.5))), + ], + ), + ], + ), + Text( + feedbackAdditionalInfo + .senderData!.account_banned + ? "Banned" + : "Active", + style: TextStyle( + color: feedbackAdditionalInfo + .senderData! + .account_banned + ? Colors.red + : Color(widget + .route.routeColor))), + ], + ), + const SizedBox( + height: Constants.defaultPadding * 2), + Center( + child: Container( + width: 500, + padding: const EdgeInsets.all( + Constants.defaultPadding * 2), + decoration: BoxDecoration( + border: Border.all( + width: 2, + color: Colors.white + .withValues(alpha: 0.5)), + borderRadius: BorderRadius.circular( + Constants.defaultPadding / 2)), + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, children: [ - const Text( - "Driver", - textAlign: TextAlign.right, - maxLines: 1, - overflow: - TextOverflow.ellipsis, - ), + Text(DateFormat('MMMM d, y') + .format(selectedFeedback! + .timestamp + .toDate())), Text( - feedbackAdditionalInfo - .recepientData != - null - ? feedbackAdditionalInfo - .recepientData! - .account_name - : "No Data", - textAlign: TextAlign.right, - maxLines: 1, - overflow: - TextOverflow.ellipsis, - ), + DateFormat('hh:mm a') + .format( + selectedFeedback! + .timestamp + .toDate()), + style: TextStyle( + fontSize: 13, + color: Colors.white + .withValues( + alpha: 0.5))), ], ), - ), - const SizedBox( - width: - Constants.defaultPadding * - 2), - Expanded( - child: Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - const Text( - "Jeepney", - textAlign: TextAlign.right, - maxLines: 1, - overflow: - TextOverflow.ellipsis, - ), - Text( - selectedFeedback! - .feedback_jeepney, - textAlign: TextAlign.right, - maxLines: 1, - overflow: - TextOverflow.ellipsis, - ), - ], + TextButton.icon( + onPressed: () { + AwesomeDialog( + context: context, + width: 400, + dialogType: + DialogType.warning, + padding: const EdgeInsets + .all(Constants + .defaultPadding), + desc: + "You are about to delete this feedback. This action cannot be undone.", + btnOkText: "Delete", + btnOkColor: Colors.red[600], + btnCancelText: "Cancel", + btnCancelColor: + Constants.bgColor, + btnCancelOnPress: () {}, + btnOkOnPress: () async { + await deleteFeedback( + selectedFeedback! + .feedback_sender, + selectedFeedback! + .timestamp); + + await AwesomeDialog( + context: context, + width: 150, + padding: const EdgeInsets + .only( + bottom: Constants + .defaultPadding), + dialogType: + DialogType + .noHeader, + body: CircularProgressIndicator( + color: Color( + widget + .route + .routeColor)), + dismissOnBackKeyPress: + false, + dismissOnTouchOutside: + false, + autoHide: + const Duration( + milliseconds: + 1000)) + .show(); + loadFeedbacks(); + }, + ).show(); + }, + icon: Icon(Icons.delete, + color: Colors.red[600]), + label: Text("Delete", + style: TextStyle( + color: + Colors.red[600])), + ) + ], + ), + const SizedBox( + height: + Constants.defaultPadding / 2), + const Divider(color: Colors.white), + const SizedBox( + height: + Constants.defaultPadding / 2), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + feedbackAdditionalInfo + .recepientData != + null + ? feedbackAdditionalInfo + .recepientData! + .account_name + : "No Data", + textAlign: TextAlign.right, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - ), - ], - ), - const SizedBox( - height: - Constants.defaultPadding / 2), - Row( - children: [ - Expanded( - child: Row( - mainAxisAlignment: - MainAxisAlignment.end, - children: - List.generate(5, (index) { - return Icon( - 4 - index < - selectedFeedback! - .feedback_driving_rating - ? Icons.star - : Icons.star_border, - color: 4 - index < - selectedFeedback! - .feedback_driving_rating - ? Color(widget - .route.routeColor) - : Colors.grey, - size: 20, - ); - }), + Expanded( + child: Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: + List.generate(5, (index) { + return Icon( + 4 - index < + selectedFeedback! + .feedback_driving_rating + ? Icons.star + : Icons.star_border, + color: 4 - index < + selectedFeedback! + .feedback_driving_rating + ? Color(widget + .route.routeColor) + : Colors.grey, + ); + }), + ), ), - ), - const SizedBox( - width: - Constants.defaultPadding * - 2), - Expanded( - child: Row( - mainAxisAlignment: - MainAxisAlignment.end, - children: - List.generate(5, (index) { - return Icon( - 4 - index < - selectedFeedback! - .feedback_jeepney_rating - ? Icons.star - : Icons.star_border, - color: 4 - index < - selectedFeedback! - .feedback_jeepney_rating - ? Color(widget - .route.routeColor) - : Colors.grey, - size: 20, - ); - }), + ], + ), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + selectedFeedback! + .feedback_jeepney, + textAlign: TextAlign.right, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - ), - ], - ), - const SizedBox( - height: - Constants.defaultPadding / 2), - const Divider(color: Colors.white), - const SizedBox( - height: Constants.defaultPadding), - RichText( - textAlign: TextAlign.justify, - text: TextSpan( + Expanded( + child: Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: + List.generate(5, (index) { + return Icon( + 4 - index < + selectedFeedback! + .feedback_jeepney_rating + ? Icons.star + : Icons.star_border, + color: 4 - index < + selectedFeedback! + .feedback_jeepney_rating + ? Color(widget + .route.routeColor) + : Colors.grey, + ); + }), + ), + ), + ], + ), + const SizedBox( + height: + Constants.defaultPadding / 2), + const Divider(color: Colors.white), + const SizedBox( + height: + Constants.defaultPadding / 2), + RichText( + textAlign: TextAlign.justify, + text: TextSpan( + children: [ + TextSpan( + text: selectedFeedback! + .feedback_content, + style: const TextStyle( + color: Colors.white, + fontStyle: + FontStyle.italic, + fontWeight: + FontWeight.w200)), + ], + )), + if (selectedFeedback!.feedback_img != + null && + selectedFeedback! + .feedback_img!.isNotEmpty) + Column( children: [ - const WidgetSpan( - child: - SizedBox(width: 40.0)), - TextSpan( - text: selectedFeedback! - .feedback_content, - style: const TextStyle( - color: Colors.white, - fontStyle: - FontStyle.italic, - fontWeight: - FontWeight.w200)), + const SizedBox( + height: Constants + .defaultPadding), + SizedBox( + height: 200, + // Image.network displays img from URL + child: Image.network( + selectedFeedback! + .feedback_img!, + fit: BoxFit.contain, + errorBuilder: + (BuildContext context, + Object error, + StackTrace? + stackTrace) { + return const Text( + 'Failed to load image', + style: TextStyle( + color: + Colors.red), + ); + }, + )), ], - )), - ], + ) + ], + ), ), ), - ), - const Spacer(), - const Divider(color: Colors.white), - ], + // const Spacer(), + ], + ), ), ); }) : const SizedBox( - height: 700, + height: 500, child: Center( child: Logo(), ))) diff --git a/transitrack_web/lib/components/account_related/route_manager/data_visualization/filters.dart b/transitrack_web/lib/components/account_related/route_manager/data_visualization/filters.dart index 238380b..bc30b33 100644 --- a/transitrack_web/lib/components/account_related/route_manager/data_visualization/filters.dart +++ b/transitrack_web/lib/components/account_related/route_manager/data_visualization/filters.dart @@ -123,9 +123,6 @@ class FiltersState extends State { ], ), const SizedBox(height: Constants.defaultPadding), - const SizedBox(height: Constants.defaultPadding), - const Divider(color: Colors.white), - const SizedBox(height: Constants.defaultPadding), Button( onTap: () { widget.newFilter(FilterParameters( diff --git a/transitrack_web/lib/components/account_related/route_manager/data_visualization/jeep_historical.dart b/transitrack_web/lib/components/account_related/route_manager/data_visualization/jeep_historical.dart index 6af0b22..84c14fe 100644 --- a/transitrack_web/lib/components/account_related/route_manager/data_visualization/jeep_historical.dart +++ b/transitrack_web/lib/components/account_related/route_manager/data_visualization/jeep_historical.dart @@ -68,7 +68,7 @@ class _JeepHistoricalPageState extends State { Widget build(BuildContext context) { return SizedBox( width: double.maxFinite, - height: 700, + height: 500, child: Stack( children: [ JeepHistoricalMap( diff --git a/transitrack_web/lib/components/account_related/route_manager/data_visualization/manage_commuters_table.dart b/transitrack_web/lib/components/account_related/route_manager/data_visualization/manage_commuters_table.dart new file mode 100644 index 0000000..2d2ca4b --- /dev/null +++ b/transitrack_web/lib/components/account_related/route_manager/data_visualization/manage_commuters_table.dart @@ -0,0 +1,277 @@ +import 'package:awesome_dialog/awesome_dialog.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:transitrack_web/components/account_related/route_manager/data_visualization/filters.dart'; +import 'package:transitrack_web/components/account_related/route_manager/data_visualization/selected_commuter_details.dart'; +import 'package:transitrack_web/components/left_drawer/logo.dart'; +import 'package:transitrack_web/models/account_model.dart'; +import 'package:transitrack_web/models/feedback_model.dart'; +import 'package:transitrack_web/models/filter_model.dart'; +import 'package:transitrack_web/models/report_model.dart'; +import 'package:transitrack_web/models/route_model.dart'; +import 'package:transitrack_web/style/constants.dart'; + +class ManageCommutersTable extends StatefulWidget { + final RouteData route; + const ManageCommutersTable({super.key, required this.route}); + + @override + State createState() => _ManageCommutersTableState(); +} + +class CommuterFeedbackAndReport { + List? feedback; + List? report; + + CommuterFeedbackAndReport({ + required this.feedback, + required this.report, + }); +} + +class _ManageCommutersTableState extends State { + TextEditingController searchController = TextEditingController(); + + int selected = -1; + AccountData? selectedCommuter; + List routes = []; + + List? commuters; + + String searchString = ""; + + FilterParameters orderBy = + FilterParameters(filterSearch: "account_name", filterDescending: true); + + @override + void initState() { + super.initState(); + + loadRoutes(); + } + + void select(int index, AccountData? account) { + setState(() { + selected = index; + selectedCommuter = account; + }); + } + + Future loadRoutes() async { + routes.clear(); + + Query> query = + FirebaseFirestore.instance.collection('routes').orderBy("route_id"); + + QuerySnapshot querySnapshot = await query.get(); + + setState(() { + routes = querySnapshot.docs.map((DocumentSnapshot document) { + return RouteData.fromFirestore(document); + }).toList(); + }); + } + + Future loadCommuters() async { + setState(() { + commuters = null; + }); + select(-1, null); + + Query> query = FirebaseFirestore.instance + .collection('accounts') + // filter to only include commuter accounts + .where('account_type', isEqualTo: 0); + + query = query.orderBy(orderBy.filterSearch, + descending: orderBy.filterDescending); + + QuerySnapshot querySnapshot = await query.get(); + + setState(() { + commuters = querySnapshot.docs.map((DocumentSnapshot document) { + return AccountData.fromSnapshot(document); + }).toList(); + }); + } + + void onSearchChanged(String value) { + setState(() { + searchString = value; + }); + + if (value.isNotEmpty) { + if (commuters == null) { + loadCommuters(); + } + } else { + // Clear the list when search is empty + setState(() { + commuters = null; + }); + } + + select(-1, null); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: Constants.defaultPadding), + child: Row( + children: [ + SizedBox( + height: 500, + width: 500, + child: Column( + children: [ + SearchBar( + controller: searchController, + overlayColor: WidgetStateProperty.all( + Colors.white.withValues(alpha: 0.2)), + elevation: WidgetStateProperty.all(0.0), + onChanged: onSearchChanged, + hintText: 'Search Account Name', + hintStyle: WidgetStateProperty.all( + TextStyle(color: Color(widget.route.routeColor))), + leading: const Icon(Icons.search), + shape: + WidgetStateProperty.all(const ContinuousRectangleBorder( + borderRadius: BorderRadius.zero, + )), + trailing: [ + Row( + children: [ + IconButton( + onPressed: () { + if (searchString.isNotEmpty) { + // Reload commuters based on the current search string + loadCommuters(); + } else { + // Clear commuters when search bar is empty + setState(() { + commuters = null; + }); + } + }, + icon: const Icon(Icons.refresh), + ), + IconButton( + onPressed: () => AwesomeDialog( + dialogType: DialogType.noHeader, + context: (context), + width: 500, + body: PointerInterceptor( + child: Filters( + route: widget.route, + dropdownList: FilterParameters.commutersOrderBy, + oldFilter: orderBy, + newFilter: (FilterParameters newFilter) { + setState(() { + orderBy = newFilter; + }); + loadCommuters(); + }, + )), + ).show(), + icon: const Icon(Icons.filter_list), + ) + ], + ) + ], + ), + if (commuters != null) + const Padding( + padding: EdgeInsets.fromLTRB( + Constants.defaultPadding, + Constants.defaultPadding, + Constants.defaultPadding, + 0.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [Text("Name"), Text("Status")]), + ), + if (commuters != null) + Expanded( + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: ListView.builder( + shrinkWrap: true, + itemCount: commuters!.length, + itemBuilder: (context, index) { + AccountData commuter = commuters![index]; + + if (searchString.isNotEmpty && + !commuter.account_name + .toLowerCase() + .contains(searchString.toLowerCase())) { + return const SizedBox(); + } + + return ListTile( + onTap: () async { + if (selected == index) { + select(-1, null); + } else { + select(index, commuter); + } + }, + selected: index == selected, + selectedColor: Colors.white, + selectedTileColor: Color(widget.route.routeColor) + .withValues(alpha: 0.1), + hoverColor: Colors.white.withValues(alpha: 0.2), + subtitleTextStyle: TextStyle( + color: Colors.grey.withValues(alpha: 0.75), + fontStyle: FontStyle.italic, + fontSize: 12), + title: Text( + commuter.account_name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + "<${commuter.account_email}>", + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: Icon( + commuter.account_banned + ? Icons.no_accounts_outlined + : Icons.account_circle_outlined, + color: commuter.account_banned + ? Colors.red[600] + : Colors.blue, + size: 20, + ), + ); + }, + ), + ), + ), + ], + ), + ), + const SizedBox(width: Constants.defaultPadding), + Expanded( + child: SizedBox( + height: 500, + child: SingleChildScrollView( + child: Center( + child: selectedCommuter != null + ? SelectedCommuterDetails( + commuter: selectedCommuter!, + routes: routes, + route: widget.route, + loadCommuters: () => loadCommuters(), + ) + : const Logo(), + ), + ), + ), + ) + ], + )); + } +} diff --git a/transitrack_web/lib/components/account_related/route_manager/data_visualization/manage_drivers_table.dart b/transitrack_web/lib/components/account_related/route_manager/data_visualization/manage_drivers_table.dart index d8bd8eb..47211b2 100644 --- a/transitrack_web/lib/components/account_related/route_manager/data_visualization/manage_drivers_table.dart +++ b/transitrack_web/lib/components/account_related/route_manager/data_visualization/manage_drivers_table.dart @@ -105,7 +105,7 @@ class _ManageDriversTableState extends State { child: Row( children: [ SizedBox( - height: 700, + height: 500, width: 500, child: Column( children: [ @@ -166,8 +166,11 @@ class _ManageDriversTableState extends State { ), if (drivers != null) const Padding( - padding: EdgeInsets.symmetric( - horizontal: Constants.defaultPadding), + padding: EdgeInsets.fromLTRB( + Constants.defaultPadding, + Constants.defaultPadding, + Constants.defaultPadding, + 0.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -183,7 +186,7 @@ class _ManageDriversTableState extends State { ), if (drivers == null) SizedBox( - height: 500, + height: 300, child: Center( child: CircularProgressIndicator( color: Color(widget.route.routeColor), @@ -266,15 +269,20 @@ class _ManageDriversTableState extends State { ), const SizedBox(width: Constants.defaultPadding), Expanded( - child: Center( - child: selectedDriver != null - ? SelectedDriverDetails( - driver: selectedDriver!, - routes: routes, - route: widget.route, - loadDrivers: () => loadDrivers(), - ) - : const Logo(), + child: SizedBox( + height: 500, + child: SingleChildScrollView( + child: Center( + child: selectedDriver != null + ? SelectedDriverDetails( + driver: selectedDriver!, + routes: routes, + route: widget.route, + loadDrivers: () => loadDrivers(), + ) + : const Logo(), + ), + ), ), ) ], diff --git a/transitrack_web/lib/components/account_related/route_manager/data_visualization/reports_table.dart b/transitrack_web/lib/components/account_related/route_manager/data_visualization/reports_table.dart index 63ac180..b126f86 100644 --- a/transitrack_web/lib/components/account_related/route_manager/data_visualization/reports_table.dart +++ b/transitrack_web/lib/components/account_related/route_manager/data_visualization/reports_table.dart @@ -6,12 +6,16 @@ import 'package:mapbox_gl/mapbox_gl.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:transitrack_web/components/account_related/route_manager/data_visualization/filters.dart'; import 'package:transitrack_web/components/account_related/route_manager/data_visualization/reports_map.dart'; +import 'package:transitrack_web/components/acknowledge_report.dart'; +import 'package:transitrack_web/components/attach_img_button.dart'; +import 'package:transitrack_web/components/text_field.dart'; import 'package:transitrack_web/models/account_model.dart'; import 'package:transitrack_web/models/feedback_model.dart'; import 'package:transitrack_web/models/filter_model.dart'; import 'package:transitrack_web/models/report_model.dart'; import 'package:transitrack_web/models/route_model.dart'; import 'package:transitrack_web/style/constants.dart'; +import 'package:url_launcher/url_launcher.dart'; // This widget is used in the Reports tab of the Data visualization panel of the route manager to list all the reports issued in the route. @@ -27,6 +31,10 @@ class ReportsTable extends StatefulWidget { class _ReportsTableState extends State { TextEditingController searchController = TextEditingController(); + // For report acknowledgement + // TextEditingController recipientEmailController = TextEditingController(); + TextEditingController emailController = TextEditingController(); + bool mapLoaded = false; int selected = -1; late ReportData? selectedReport; @@ -89,6 +97,45 @@ class _ReportsTableState extends State { } } + // display image as a dialog for RM + void viewImg(String imgUrl) { + AwesomeDialog( + context: context, + dialogType: DialogType.noHeader, + width: 1000, + body: PointerInterceptor( + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(30.0), + child: Image.network( + imgUrl, + fit: BoxFit.contain, + loadingBuilder: (BuildContext context, Widget child, + ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) { + return child; + } + return Center(child: CircularProgressIndicator()); + }, + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Text( + 'Failed to load image', + style: TextStyle(color: Colors.red), + ); + }, + ), + ), + ], + ), + ), + showCloseIcon: true, + dismissOnBackKeyPress: true, + dismissOnTouchOutside: true, + ).show(); + } + @override Widget build(BuildContext context) { return Padding( @@ -96,7 +143,7 @@ class _ReportsTableState extends State { child: Row( children: [ SizedBox( - height: 700, + height: 500, width: 500, child: Column( children: [ @@ -157,7 +204,7 @@ class _ReportsTableState extends State { ), if (reports == null || !mapLoaded) SizedBox( - height: 500, + height: 300, child: Center( child: CircularProgressIndicator( color: Color(widget.route.routeColor), @@ -228,7 +275,7 @@ class _ReportsTableState extends State { const SizedBox(width: Constants.defaultPadding), Expanded( child: SizedBox( - height: 700, + height: 500, child: Stack( children: [ ReportsMap( @@ -253,7 +300,15 @@ class _ReportsTableState extends State { Positioned( right: Constants.defaultPadding, top: Constants.defaultPadding, - child: ReportContents(reportData: selectedReport!)), + child: ReportContents( + reportData: selectedReport!, + viewImg: viewImg, + loadReports: loadReports, + acknowledgeReport: (report) => acknowledgeReport( + context, + report, + emailController, + widget.route.routeName))), const Positioned( right: Constants.defaultPadding, bottom: Constants.defaultPadding * 2, @@ -295,7 +350,32 @@ class Legends extends StatelessWidget { class ReportContents extends StatelessWidget { final ReportData reportData; - const ReportContents({super.key, required this.reportData}); + final Function(String) viewImg; + final Function loadReports; + final Function(ReportData) acknowledgeReport; + const ReportContents( + {super.key, + required this.reportData, + required this.viewImg, + required this.loadReports, + required this.acknowledgeReport}); + + Future deleteReport(String senderEmail, Timestamp timestamp) async { + try { + QuerySnapshot querySnapshot = await FirebaseFirestore.instance + .collection('reports') + .where('report_sender', isEqualTo: senderEmail) + .where('timestamp', isEqualTo: timestamp) + .limit(1) + .get(); + + if (querySnapshot.docs.isNotEmpty) { + await querySnapshot.docs.first.reference.delete(); + } + } catch (error) { + debugPrint("Error deleting report: $error"); + } + } @override Widget build(BuildContext context) { @@ -328,30 +408,78 @@ class ReportContents extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(ReportData - .reportDetails[reportData.report_type].reportType), - const SizedBox(width: Constants.defaultPadding), - Text(DateFormat('MMM d, y') - .format(reportData.timestamp.toDate())), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(DateFormat('MMM d, y') + .format(reportData.timestamp.toDate())), + Text( + DateFormat('hh:mm a') + .format(reportData.timestamp.toDate()), + style: TextStyle( + fontSize: 13, + color: Colors.white.withValues(alpha: 0.5))), + ], + ), + TextButton.icon( + onPressed: () { + AwesomeDialog( + context: context, + width: 400, + dialogType: DialogType.warning, + padding: const EdgeInsets.all(Constants.defaultPadding), + desc: + "You are about to delete this report. This action cannot be undone.", + btnOkText: "Delete", + btnOkColor: Colors.red[600], + btnCancelText: "Cancel", + btnCancelColor: Constants.bgColor, + btnCancelOnPress: () {}, + btnOkOnPress: () async { + await deleteReport( + reportData.report_sender, reportData.timestamp); + + await AwesomeDialog( + context: context, + width: 150, + padding: const EdgeInsets.only( + bottom: Constants.defaultPadding), + dialogType: DialogType.noHeader, + body: CircularProgressIndicator( + color: Colors.white), + dismissOnBackKeyPress: false, + dismissOnTouchOutside: false, + autoHide: const Duration(milliseconds: 1000)) + .show(); + loadReports(); + }, + ).show(); + }, + icon: Icon(Icons.delete, color: Colors.red[600]), + label: Text("Delete", + style: TextStyle(color: Colors.red[600])), + ) ], ), + const SizedBox(height: Constants.defaultPadding / 2), + const Divider(color: Colors.white), + const SizedBox(height: Constants.defaultPadding / 2), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Expanded( + child: Text(ReportData + .reportDetails[reportData.report_type].reportType), + ), if (reportData.report_type > 0 && reportData.report_type < 4) - Text(usersAdditionalInfo.locationData!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13, - color: Colors.white.withValues(alpha: 0.5))), - const SizedBox(width: Constants.defaultPadding), - Text( - DateFormat('hh:mm a') - .format(reportData.timestamp.toDate()), - style: TextStyle( - fontSize: 13, - color: Colors.white.withValues(alpha: 0.5))), + Expanded( + child: Text(usersAdditionalInfo.locationData!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13, + color: Colors.white.withValues(alpha: 0.5))), + ), ], ), const SizedBox(height: Constants.defaultPadding / 2), @@ -364,7 +492,6 @@ class ReportContents extends StatelessWidget { textAlign: TextAlign.justify, text: TextSpan( children: [ - const WidgetSpan(child: SizedBox(width: 40.0)), TextSpan( text: reportData.report_content, style: const TextStyle( @@ -373,6 +500,22 @@ class ReportContents extends StatelessWidget { fontWeight: FontWeight.w200)), ], )), + const SizedBox(height: Constants.defaultPadding / 2), + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 5.0, + children: [ + AttachmentButton( + onPressed: () => acknowledgeReport(reportData), + label: "Acknowledge", + icon: Icons.send), + if (reportData.report_img != null && + reportData.report_img!.isNotEmpty) + AttachmentButton( + onPressed: () => viewImg(reportData.report_img!), + label: "View Image", + icon: Icons.photo), + ]), ], ), const SizedBox(height: Constants.defaultPadding / 2), diff --git a/transitrack_web/lib/components/account_related/route_manager/data_visualization/selected_commuter_details.dart b/transitrack_web/lib/components/account_related/route_manager/data_visualization/selected_commuter_details.dart new file mode 100644 index 0000000..ebc9504 --- /dev/null +++ b/transitrack_web/lib/components/account_related/route_manager/data_visualization/selected_commuter_details.dart @@ -0,0 +1,400 @@ +import 'package:awesome_dialog/awesome_dialog.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:transitrack_web/components/account_related/route_manager/data_visualization/manage_commuters_table.dart'; +import 'package:transitrack_web/components/right_panel/commuter_feedback_tab.dart'; +import 'package:transitrack_web/models/account_model.dart'; +import 'package:transitrack_web/models/feedback_model.dart'; +import 'package:transitrack_web/models/report_model.dart'; +import 'package:transitrack_web/models/route_model.dart'; +import 'package:transitrack_web/style/constants.dart'; +import 'package:cloud_functions/cloud_functions.dart'; + +class SelectedCommuterDetails extends StatefulWidget { + final List routes; + final RouteData route; + final AccountData commuter; + final Function loadCommuters; + const SelectedCommuterDetails( + {super.key, + required this.routes, + required this.route, + required this.commuter, + required this.loadCommuters}); + + @override + State createState() => + _SelectedCommuterDetailsState(); +} + +class _SelectedCommuterDetailsState extends State { + Future getFeedbackAndReport(String email) async { + List? feedback = await getFeedbackSender(email); + List? report = await getReportSender(email); + + return CommuterFeedbackAndReport(feedback: feedback, report: report); + } + + // Method to disable a user by calling the Firebase function + Future toggleCommuterStatus(String email, bool disable) async { + try { + QuerySnapshot snapshot = await FirebaseFirestore.instance + .collection('accounts') + .where('account_email', isEqualTo: email) + .limit(1) + .get(); + + // Get the UID from the 'account_id' field in the Firestore document + String uid = snapshot.docs.first.get('account_uid'); + + // Get the instance of the Firebase function + final HttpsCallable callable = + FirebaseFunctions.instance.httpsCallable('toggleUserStatus'); + + // Call the function with the user ID + await callable.call({ + 'uid': uid, + 'disable': disable, + }); + } catch (e) { + // Display the error message + errorMessage('Error ${disable ? 'banning' : 'activating'} account: $e'); + } + } + + void errorMessage(String message) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + backgroundColor: Constants.bgColor, + title: Center( + child: Text( + message, + style: const TextStyle(color: Colors.white), + ))); + }); + } + + bool showFeedback = true; + + @override + Widget build(BuildContext context) { + return Container( + width: 600, + padding: const EdgeInsets.all(Constants.defaultPadding * 2), + decoration: BoxDecoration( + border: Border.all( + width: 2, color: Colors.white.withValues(alpha: 0.5)), + borderRadius: BorderRadius.circular(Constants.defaultPadding / 2)), + child: FutureBuilder( + future: getFeedbackAndReport(widget.commuter.account_email), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center( + child: CircularProgressIndicator( + color: Color(widget.route.routeColor), + )); + } + + if (snapshot.hasError) { + return Text(snapshot.error.toString()); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + Icon( + Icons.account_circle, + color: Color(widget.route.routeColor), + size: 32, + ), + const SizedBox(width: Constants.defaultPadding), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.commuter.account_name), + Text(widget.commuter.account_email, + style: TextStyle( + fontSize: 11, + color: Colors.white + .withValues(alpha: 0.2))), + ], + ), + ], + ), + Text( + widget.commuter.account_banned + ? "Banned" + : "Active", + style: TextStyle( + color: widget.commuter.account_banned + ? Colors.red[600] + : Color(widget.route.routeColor)), + ), + ]), + const Divider(color: Colors.white), + const SizedBox(height: Constants.defaultPadding), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ToggleButtons( + isSelected: [showFeedback, !showFeedback], + onPressed: (index) { + setState(() { + showFeedback = index == 0; + }); + }, + borderRadius: BorderRadius.circular( + Constants.defaultPadding / 2), + color: Colors.white, // Default text color + selectedColor: Colors.white, + fillColor: Color(widget.route.routeColor) + .withValues(alpha: 0.2), + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: Constants.defaultPadding), + child: Text('Feedback'), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: Constants.defaultPadding), + child: Text('Reports'), + ), + ], + ), + ], + ), + if (showFeedback) + if (snapshot.hasData && + snapshot.data!.feedback!.isNotEmpty) + Padding( + padding: EdgeInsetsDirectional.symmetric( + vertical: Constants.defaultPadding), + child: FeedBack( + feedbacks: snapshot.data!.feedback!, + routes: widget.routes, + loadCommuters: widget.loadCommuters)) + else + Padding( + padding: EdgeInsetsDirectional.symmetric( + vertical: Constants.defaultPadding), + child: Center( + child: Text("No feedback yet"), + )), + if (!showFeedback) + if (snapshot.hasData && snapshot.data!.report!.isNotEmpty) + Padding( + padding: EdgeInsetsDirectional.symmetric( + vertical: Constants.defaultPadding), + child: Report( + routes: widget.routes, + reports: snapshot.data!.report!, + loadCommuters: widget.loadCommuters)) + else + Padding( + padding: EdgeInsetsDirectional.symmetric( + vertical: Constants.defaultPadding), + child: Center( + child: Text("No reports available"), + )), + const Divider(color: Colors.white), + Row( + children: [ + Expanded( + child: SizedBox( + child: IconButton( + icon: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + widget.commuter.account_banned + ? Icons.account_circle_outlined + : Icons.no_accounts_outlined, + color: widget.commuter.account_banned + ? Colors.blue + : Colors.red[600], + size: 15), + const SizedBox( + width: Constants.defaultPadding / 2), + Text(widget.commuter.account_banned + ? "Enable Account" + : "Ban Account"), + ], + ), + onPressed: () => AwesomeDialog( + context: context, + width: 400, + dialogType: widget.commuter.account_banned + ? DialogType.info + : DialogType.warning, + padding: const EdgeInsets.all( + Constants.defaultPadding), + desc: + "You are about to ${widget.commuter.account_banned ? "enable" : "ban"} ${widget.commuter.account_name}.", + btnOkText: widget.commuter.account_banned + ? "Enable" + : "Ban", + btnOkColor: widget.commuter.account_banned + ? Colors.blue + : Colors.red[600], + btnOkOnPress: () async { + await toggleCommuterStatus( + widget.commuter.account_email, + !widget.commuter.account_banned); + await AwesomeDialog( + context: context, + width: 150, + padding: const EdgeInsets.only( + bottom: + Constants.defaultPadding), + dialogType: DialogType.noHeader, + body: CircularProgressIndicator( + color: Color( + widget.route.routeColor)), + dismissOnBackKeyPress: false, + dismissOnTouchOutside: false, + autoHide: const Duration( + milliseconds: 1000)) + .show(); + widget.loadCommuters(); + }).show(), + ), + ), + ), + ], + ), + ]); + })); + } +} + +class FeedBack extends StatefulWidget { + final List routes; + final List feedbacks; + final Function loadCommuters; + const FeedBack( + {super.key, + required this.routes, + required this.feedbacks, + required this.loadCommuters}); + + @override + State createState() => FeedBackState(); +} + +class FeedBackState extends State { + int index = 0; + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + if (index > 0) { + setState(() { + index--; + }); + } + }, + icon: const Icon(Icons.arrow_left)), + Expanded( + child: CommuterFeedbackTab( + route: widget.routes[widget.feedbacks[index].feedback_route], + feedBack: widget.feedbacks[index], + loadCommuters: widget.loadCommuters, + ), + ), + IconButton( + onPressed: () { + if (index < widget.feedbacks.length - 1) { + setState(() { + index++; + }); + } + }, + icon: const Icon(Icons.arrow_right)), + ], + ), + const SizedBox(height: Constants.defaultPadding), + Text("${index + 1}/${widget.feedbacks.length}") + ], + ); + } +} + +class Report extends StatefulWidget { + final List routes; + final List reports; + final Function loadCommuters; + + const Report({ + super.key, + required this.routes, + required this.reports, + required this.loadCommuters, + }); + + @override + State createState() => ReportState(); +} + +class ReportState extends State { + int index = 0; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + if (index > 0) { + setState(() { + index--; + }); + } + }, + icon: const Icon(Icons.arrow_left), + ), + Expanded( + child: CommuterReportTab( + route: widget.routes[widget.reports[index].report_route], + report: widget.reports[index], + loadCommuters: widget.loadCommuters, + ), + ), + IconButton( + onPressed: () { + if (index < widget.reports.length - 1) { + setState(() { + index++; + }); + } + }, + icon: const Icon(Icons.arrow_right), + ), + ], + ), + const SizedBox(height: Constants.defaultPadding), + Text("${index + 1}/${widget.reports.length}") + ], + ); + } +} diff --git a/transitrack_web/lib/components/account_related/route_manager/data_visualization/selected_driver_details.dart b/transitrack_web/lib/components/account_related/route_manager/data_visualization/selected_driver_details.dart index b30eb00..a6bf25e 100644 --- a/transitrack_web/lib/components/account_related/route_manager/data_visualization/selected_driver_details.dart +++ b/transitrack_web/lib/components/account_related/route_manager/data_visualization/selected_driver_details.dart @@ -46,7 +46,7 @@ class _SelectedDriverDetailsState extends State { JeepData jeepData = JeepData.fromSnapshot(querySnapshot.docs.first); String address = await findAddress( - LatLng(jeepData.location.latitude, jeepData.location.longitude)); + LatLng(jeepData.location.latitude, jeepData.location.longitude), false); return JeepDataRatingAndAddress( jeepData: jeepData, address: address, rating: ratings); diff --git a/transitrack_web/lib/components/account_related/route_manager/data_visualization/shared_locations_page.dart b/transitrack_web/lib/components/account_related/route_manager/data_visualization/shared_locations_page.dart index c055bda..779022d 100644 --- a/transitrack_web/lib/components/account_related/route_manager/data_visualization/shared_locations_page.dart +++ b/transitrack_web/lib/components/account_related/route_manager/data_visualization/shared_locations_page.dart @@ -127,7 +127,7 @@ class _SharedLocationsPageState extends State { @override Widget build(BuildContext context) { return SizedBox( - height: 700, + height: 500, child: Stack( children: [ SharedLocationsMap( diff --git a/transitrack_web/lib/components/account_related/route_manager/register_jeep.dart b/transitrack_web/lib/components/account_related/route_manager/register_jeep.dart index 51260b7..405af43 100644 --- a/transitrack_web/lib/components/account_related/route_manager/register_jeep.dart +++ b/transitrack_web/lib/components/account_related/route_manager/register_jeep.dart @@ -62,7 +62,6 @@ class _RegisterJeepState extends State { .add({ 'device_id': jeepNameController.text.toUpperCase(), 'timestamp': FieldValue.serverTimestamp(), - 'passenger_count': 0, 'max_capacity': int.parse(jeepCapacityController.text), 'location': const GeoPoint(14.653836, 121.068427), 'route_id': widget.route.routeId, diff --git a/transitrack_web/lib/components/account_related/route_manager/route_coordinates_settings.dart b/transitrack_web/lib/components/account_related/route_manager/route_coordinates_settings.dart index 89f4878..c224a77 100644 --- a/transitrack_web/lib/components/account_related/route_manager/route_coordinates_settings.dart +++ b/transitrack_web/lib/components/account_related/route_manager/route_coordinates_settings.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:mapbox_gl/mapbox_gl.dart'; +import 'package:transitrack_web/services/find_location.dart'; import '../../../models/route_model.dart'; import '../../../style/constants.dart'; @@ -7,115 +9,274 @@ import '../../../style/constants.dart'; class CoordinatesSettings extends StatefulWidget { final RouteData route; + // used to communicate changes in mode: -1 = no mode | 0 = edit | 1 = add/delete | -2 = close/ reset final ValueChanged coordConfig; + final ValueChanged stopsConfig; + final LatLng? selectedStop; // Add selectedStop to display it + const CoordinatesSettings( - {super.key, required this.route, required this.coordConfig}); + {super.key, + required this.route, + required this.coordConfig, + required this.stopsConfig, + required this.selectedStop}); @override State createState() => _CoordinatesSettingsState(); } class _CoordinatesSettingsState extends State { - int selected = -1; + // this tracks the mode: -1 = no mode selected | 0 = edit | 1 = add/remove + int routeCoordSettingsMode = -1; + int stopsCoordSettingsMode = -1; + String? address; // To store the fetched address + + @override + void didUpdateWidget(covariant CoordinatesSettings oldWidget) { + super.didUpdateWidget(oldWidget); + + // Check if the selectedStop has changed + if (widget.selectedStop != oldWidget.selectedStop) { + _fetchAddress(); // Fetch the address for the new selectedStop + } + } + + Future _fetchAddress() async { + if (widget.selectedStop != null) { + try { + // Call the asynchronous findAddress function + String fetchedAddress = await findAddress(widget.selectedStop!, true); + + // Update the state with the fetched address + setState(() { + address = fetchedAddress; + }); + } catch (e) { + // Handle errors (e.g., network issues) + setState(() { + address = "Unable to fetch address"; + }); + } + } else { + // If no stop is selected, clear the address + setState(() { + address = null; + }); + } + } @override Widget build(BuildContext context) { return Column( - mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.start, // Align everything to the left + mainAxisAlignment: MainAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - "Coordinates", - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - GestureDetector( - onTap: () { - if (selected == -1) { - widget.coordConfig(-1); - } else { - setState(() { - selected = -1; - }); - widget.coordConfig(-2); - } - }, - child: Icon(selected == -1 - ? Icons.keyboard_backspace_outlined - : Icons.close)) - ], - ), + if (stopsCoordSettingsMode != 0 && stopsCoordSettingsMode != 1) ...[ + // For route coordinates + buildHeader('Coordinates', withBackButton: true), + const SizedBox(height: Constants.defaultPadding), + buildModeButtons( + routeCoordSettingsMode, + updateRouteCoordSettingsMode, + updateRouteCoordCancelMode, + enabled: stopsCoordSettingsMode != 0 && stopsCoordSettingsMode != 1, + ), + const SizedBox(height: Constants.defaultPadding), + ], + + // For stops coordinates + buildHeader('Usual Stops'), const SizedBox(height: Constants.defaultPadding), - Row( - children: [ - if (selected == -1 || selected == 0) - Expanded( - child: GestureDetector( - onTap: () { - if (selected == -1) { - setState(() { - selected = 0; - }); - widget.coordConfig(0); - } else if (selected == 0) { - setState(() { - selected = -1; - }); - widget.coordConfig(-1); - } - }, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: Constants.defaultPadding), - color: selected == 0 - ? Color(widget.route.routeColor) - : Color(widget.route.routeColor).withValues(alpha: 0.6), - child: Center( - child: Icon(selected == 0 ? Icons.save : Icons.edit), - ), - ), - )), - if (selected == -1 || selected == 1) - Expanded( - child: GestureDetector( - onTap: () { - if (selected == -1) { - setState(() { - selected = 1; - }); - widget.coordConfig(1); - } else if (selected == 1) { - setState(() { - selected = -1; - }); - widget.coordConfig(-1); - } - }, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: Constants.defaultPadding), - color: selected == 1 - ? Color(widget.route.routeColor) - : Color(widget.route.routeColor).withValues(alpha: 0.5), - child: Center( - child: selected == 1 - ? const Icon(Icons.save) - : const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.add), - Text("/"), - Icon(Icons.remove) - ], - )), - ), - )) - ], + buildModeButtons( + stopsCoordSettingsMode, + updateStopsCoordSettingsMode, + updateStopsCoordCancelMode, + enabled: routeCoordSettingsMode != 0 && routeCoordSettingsMode != 1, ), + + // Display the selected stop + if (widget.selectedStop != null) ...[ + const SizedBox(height: Constants.defaultPadding), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, // Align content to the left + children: [ + Text( + "Selected Stop", // Display the fetched address + style: + const TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + textAlign: TextAlign.start, + ), + Text( + address ?? "Fetching...", // Display the fetched address + style: const TextStyle(fontSize: 14), + textAlign: TextAlign.start, + ), + ], + ), + ], + ], + ); + } + + Widget buildHeader(String title, {bool withBackButton = false}) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + maxLines: 1, + overflow: TextOverflow.ellipsis), + if (routeCoordSettingsMode == -1 && + stopsCoordSettingsMode == -1 && + withBackButton) + GestureDetector( + onTap: () { + widget.coordConfig(-1); // Notify parent: No mode selected + }, + child: const Icon(Icons.keyboard_backspace_outlined), + ), + ], + ); + } + + void updateRouteCoordSettingsMode(int newMode) { + setState(() { + routeCoordSettingsMode = newMode; // Update the mode + }); + widget.coordConfig(newMode); // Notify the parent widget + } + + void updateRouteCoordCancelMode(int newMode) { + setState(() { + routeCoordSettingsMode = newMode; // Update the mode + }); + widget.coordConfig(-2); // Notify the parent widget + } + + void updateStopsCoordSettingsMode(int newMode) { + setState(() { + stopsCoordSettingsMode = newMode; // Update the mode + }); + widget.stopsConfig(newMode); // Notify the parent widget + } + + void updateStopsCoordCancelMode(int newMode) { + setState(() { + stopsCoordSettingsMode = newMode; // Update the mode + }); + widget.stopsConfig(-2); // Notify the parent widget + } + + // ... is a spread operator - it takes a list of iterable and spread them in the widget tree + // this is an alternative to using another Row for the build button save and cancel + Widget buildModeButtons( + int mode, ValueChanged onModeChange, ValueChanged onModeCancel, + {bool enabled = true}) { + return Row( + children: [ + if (mode == -1) + buildButton(Icons.edit, "Edit", 0, 0.6, onModeChange, + enabled: enabled), + if (mode == 0) ...[ + buildButton(Icons.save, "Save", -1, 0.6, onModeChange), + buildCancelButton(-1, onModeCancel), + ], + if (mode == -1) + buildButton( + null, + "Add/Remove", + 1, + 0.5, + onModeChange, + enabled: enabled, + isAddRemove: true, + ), + if (mode == 1) ...[ + buildButton(Icons.save, "Save", -1, 0.6, onModeChange), + buildCancelButton(-1, onModeCancel), + ], ], ); } + + Widget buildButton(IconData? icon, String label, int newMode, double alpha, + ValueChanged onModeChange, + {bool isAddRemove = false, bool enabled = true}) { + return Expanded( + child: GestureDetector( + onTap: enabled + ? () { + onModeChange(newMode); + } + : null, // Disable onTap if the button is not enabled + child: Container( + padding: + const EdgeInsets.symmetric(vertical: Constants.defaultPadding), + + color: enabled + ? Color(widget.route.routeColor).withValues(alpha: alpha) + : Color(widget.route.routeColor) + .withValues(alpha: 0.1), // Highlight active mode + child: Center( + child: isAddRemove + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.add, + color: enabled + ? Colors.white + : Colors.white.withValues( + alpha: 0.1), // Reduced opacity when disabled + ), + Text( + "/", + style: TextStyle( + color: enabled + ? Colors.white + : Colors.white.withValues( + alpha: 0.1), // Reduced opacity when disabled + ), + ), + Icon( + Icons.remove, + color: enabled + ? Colors.white + : Colors.white.withValues( + alpha: 0.1), // Reduced opacity when disabled + ), + ], + ) + : Icon( + icon, + color: enabled + ? Colors.white + : Colors.white.withValues( + alpha: 0.1), // Reduced opacity when disabled + ), + ), + ), + ), + ); + } + + Widget buildCancelButton(int newMode, ValueChanged config) { + return Expanded( + child: GestureDetector( + onTap: () { + config(newMode); // Notify parent: Close/reset configuration + }, + child: Container( + padding: + const EdgeInsets.symmetric(vertical: Constants.defaultPadding), + color: Color(widget.route.routeColor).withValues(alpha: 0.5), + child: const Center( + child: Icon(Icons.close, color: Colors.white), + ), + ), + ), + ); + } } diff --git a/transitrack_web/lib/components/account_related/route_manager/route_manager_options.dart b/transitrack_web/lib/components/account_related/route_manager/route_manager_options.dart index a421123..a548257 100644 --- a/transitrack_web/lib/components/account_related/route_manager/route_manager_options.dart +++ b/transitrack_web/lib/components/account_related/route_manager/route_manager_options.dart @@ -8,6 +8,7 @@ import '../../../models/route_model.dart'; import '../../../style/constants.dart'; import '../../account_related/route_manager/route_vehicles_settings.dart'; import '../../../models/jeep_model.dart'; +import 'package:mapbox_gl/mapbox_gl.dart'; // This widget allows the route manager to modify the route's properties, coordinates, and PUVs @@ -15,13 +16,17 @@ class RouteManagerOptions extends StatefulWidget { final RouteData route; final List jeeps; final ValueChanged coordConfig; + final ValueChanged stopsConfig; + final LatLng? selectedStop; final ValueChanged pressedJeep; const RouteManagerOptions( {super.key, required this.route, required this.jeeps, required this.pressedJeep, - required this.coordConfig}); + required this.coordConfig, + required this.stopsConfig, + required this.selectedStop}); @override State createState() => _RouteManagerOptionsState(); @@ -55,7 +60,9 @@ class _RouteManagerOptionsState extends State { selected = -1; optionTitle = "Route Management"; }); + // No mode selected for route coordinates settings widget.coordConfig(-2); + widget.stopsConfig(-2); }, child: const Icon( Icons.keyboard_backspace_outlined, @@ -187,17 +194,33 @@ class _RouteManagerOptionsState extends State { child: PropertiesSettings(route: widget.route), ), ), + // if the modify coordinates is selected if (selected == 1) - CoordinatesSettings( - route: widget.route, - coordConfig: (int coordConfig) { - if (coordConfig == -1) { - setState(() { - selected = -1; - }); - } - widget.coordConfig(coordConfig); - }), + Column( + children: [ + CoordinatesSettings( + route: widget.route, + coordConfig: (int coordConfig) { + if (coordConfig == -1) { + setState(() { + selected = -1; + }); + } + widget.coordConfig(coordConfig); + }, + stopsConfig: (int stopsConfig) { + if (stopsConfig == -1) { + setState(() { + selected = -1; + }); + } + widget.stopsConfig(stopsConfig); + }, + selectedStop: + widget.selectedStop, // Pass the actual LatLng? object + ), + ], + ), if (selected == 2) SizedBox( height: 250, diff --git a/transitrack_web/lib/components/account_related/route_manager/route_properties_settings.dart b/transitrack_web/lib/components/account_related/route_manager/route_properties_settings.dart index 2d50581..ad65b54 100644 --- a/transitrack_web/lib/components/account_related/route_manager/route_properties_settings.dart +++ b/transitrack_web/lib/components/account_related/route_manager/route_properties_settings.dart @@ -35,7 +35,7 @@ class _PropertiesSettingsState extends State { super.initState(); nameController.text = widget.route.routeName; fareController.text = widget.route.routeFare.toString(); - fareDiscountedController.text = widget.route.routeFareDiscounted.toString(); + fareDiscountedController.text = widget.route.perKmRate.toString(); enabled = widget.route.enabled; route_time = widget.route.routeTime; selectedRange = @@ -182,7 +182,7 @@ class _PropertiesSettingsState extends State { type: TextInputType.number, ), const SizedBox(height: Constants.defaultPadding), - const Text("Students, PWDs, & Senior Citizens"), + const Text("Per km rate"), InputTextField( controller: fareDiscountedController, hintText: "SPS", diff --git a/transitrack_web/lib/components/account_related/signup_form.dart b/transitrack_web/lib/components/account_related/signup_form.dart index c2489e1..802ee85 100644 --- a/transitrack_web/lib/components/account_related/signup_form.dart +++ b/transitrack_web/lib/components/account_related/signup_form.dart @@ -4,12 +4,12 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:transitrack_web/models/route_model.dart'; +import 'package:transitrack_web/components/signup_form_field.dart'; import '../../models/account_model.dart'; import '../../style/constants.dart'; import '../../style/style.dart'; import '../button.dart'; -import '../text_field.dart'; // Register Page @@ -22,6 +22,8 @@ class SignupForm extends StatefulWidget { } class _SignupFormState extends State { + final _formKey = GlobalKey(); // Key to manage the form state + List registerPrompts = [ "Congratulations on registering your commuter account!\n\nTo access the commuter features, please verify your account by clicking the link we've sent to your email\ninbox/spam folder.", "Congratulations on registering your driver account!\n\nTo access the driver features, contact your route manager for verification and install the JeePS Driver App.", @@ -38,6 +40,12 @@ class _SignupFormState extends State { final confirmPasswordController = TextEditingController(); String accountType = "Commuter"; + // focus node for forms + final nameFocusNode = FocusNode(); + final emailFocusNode = FocusNode(); + final passwordFocusNode = FocusNode(); + final confirmPasswordFocusNode = FocusNode(); + @override void initState() { super.initState(); @@ -45,6 +53,21 @@ class _SignupFormState extends State { fetchRoutes(); } + @override + // to avoid memory leakage + void dispose() { + // Dispose controllers and focus nodes + emailController.dispose(); + emailFocusNode.dispose(); + nameController.dispose(); + nameFocusNode.dispose(); + passwordController.dispose(); + passwordFocusNode.dispose(); + confirmPasswordController.dispose(); + confirmPasswordFocusNode.dispose(); + super.dispose(); + } + void fetchRoutes() async { List? data = await RouteData.fetchRoutes(); @@ -57,79 +80,91 @@ class _SignupFormState extends State { // sign user in method void signUserUp() async { - // show loading circle + // Show loading circle showDialog( context: context, builder: (context) { return const Center(child: CircularProgressIndicator()); }); - // try sign up + // Try sign up try { - if (nameController.text.isNotEmpty) { - // check if password is confirmed - if (passwordController.text == confirmPasswordController.text) { - if (routes != null && chosenRoute != null) { - await FirebaseAuth.instance - .createUserWithEmailAndPassword( - email: emailController.text, - password: passwordController.text) - .then((value) async { - await FirebaseFirestore.instance.collection('accounts').add({ - 'account_name': nameController.text, - 'account_email': emailController.text, - 'account_type': AccountData.accountTypeMap[accountType], - 'jeep_driving': "", - 'is_verified': false, - 'route_id': routes! - .firstWhere((element) => element.routeName == chosenRoute!) - .routeId, - }); + // Validate email first + if (emailController.text.isEmpty || + validateEmail(emailController.text) != null) { + Navigator.pop(context); // Pop loading circle + errorMessage("Enter a valid email address"); + return; + } - if (AccountData.accountTypeMap[accountType] == 0) { - value.user!.sendEmailVerification(); - } + // Validate name + if (nameController.text.isEmpty || nameController.text.length < 3) { + Navigator.pop(context); // Pop loading circle + errorMessage("Name should be at least 3 characters"); + return; + } - // pop loading circle - Navigator.pop(context); - }); + // Check if password is confirmed + if (passwordController.text != confirmPasswordController.text) { + Navigator.pop(context); // Pop loading circle + errorMessage("Passwords don't match!"); + return; + } - AwesomeDialog( - context: context, - dialogType: DialogType.info, - padding: const EdgeInsets.only( - left: Constants.defaultPadding, - right: Constants.defaultPadding, - bottom: Constants.defaultPadding), - width: 400, - onDismissCallback: (_) => Navigator.pop(context), - body: PointerInterceptor( - child: Text( - registerPrompts[AccountData.accountTypeMap[accountType]!], - textAlign: TextAlign.center, - ), - )).show(); - } else { - Navigator.pop(context); - // password dont match - errorMessage("Select a route you wish to associate to."); - } - } else { - // pop loading circle - Navigator.pop(context); + // Check if a route is selected + if (routes == null || chosenRoute == null) { + Navigator.pop(context); // Pop loading circle + errorMessage("Select a route you wish to associate to."); + return; + } + + // Proceed with Firebase sign-up + await FirebaseAuth.instance + .createUserWithEmailAndPassword( + email: emailController.text, password: passwordController.text) + .then((value) async { + String uid = value.user!.uid; + + await FirebaseFirestore.instance.collection('accounts').add({ + 'account_name': nameController.text, + 'account_email': emailController.text, + 'account_uid': uid, + 'account_type': AccountData.accountTypeMap[accountType], + 'jeep_driving': "", + 'is_verified': false, + 'route_id': routes! + .firstWhere((element) => element.routeName == chosenRoute!) + .routeId, + 'show_discounted': false, + 'account_banned': false, + }); - // password dont match - errorMessage("Passwords don't match!"); + if (AccountData.accountTypeMap[accountType] == 0) { + value.user!.sendEmailVerification(); } - } else { - // pop loading circle + + // Pop loading circle Navigator.pop(context); + }); - // password dont match - errorMessage("Name is required!"); - } + // Show success dialog + AwesomeDialog( + context: context, + dialogType: DialogType.info, + padding: const EdgeInsets.only( + left: Constants.defaultPadding, + right: Constants.defaultPadding, + bottom: Constants.defaultPadding), + width: 400, + onDismissCallback: (_) => Navigator.pop(context), + body: PointerInterceptor( + child: Text( + registerPrompts[AccountData.accountTypeMap[accountType]!], + textAlign: TextAlign.center, + ), + )).show(); } on FirebaseAuthException catch (e) { - // pop loading circle + // Pop loading circle Navigator.pop(context); errorMessage(e.code); } @@ -149,6 +184,51 @@ class _SignupFormState extends State { }); } + // Validator for name + String? validateName(String? name) { + if (name == null) { + return null; + } + if (name.length < 3 && name.isNotEmpty) { + return 'Name should be at least 3 characters'; + } + return null; // Valid input + } + + // Validator for email + String? validateEmail(String? email) { + if (email == null) { + return null; + } + if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(email) && email.isNotEmpty) { + return 'Enter a valid email address'; + } + return null; // Valid input + } + + // Validator for password + String? validatePassword(String? password) { + if (password == null) { + return null; + } + if (password.length < 6 && password.isNotEmpty) { + return 'Password must be at least 6 characters long'; + } + return null; // Valid input + } + + // Validator for confirm password + String? validateConfirmPassword(String? confirmPassword) { + if (confirmPassword == null) { + return null; + } + if (confirmPassword != passwordController.text && + confirmPassword.isNotEmpty) { + return 'Passwords do not match'; + } + return null; // Valid input + } + @override Widget build(BuildContext context) { return Padding( @@ -156,85 +236,54 @@ class _SignupFormState extends State { left: Constants.defaultPadding, right: Constants.defaultPadding, bottom: Constants.defaultPadding), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Row( - children: [ - PrimaryText( - text: "Sign Up", - color: Colors.white, - size: 40, - fontWeight: FontWeight.w700, - ) - ], - ), - const SizedBox(height: Constants.defaultPadding), - InputTextField( + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Row( + children: [ + PrimaryText( + text: "Sign Up", + color: Colors.white, + size: 40, + fontWeight: FontWeight.w700, + ) + ], + ), + const SizedBox(height: Constants.defaultPadding), + SignupFormField( controller: emailController, hintText: "Email", - obscureText: false), - const SizedBox(height: Constants.defaultPadding), - InputTextField( - controller: nameController, hintText: "Name", obscureText: false), - const SizedBox(height: Constants.defaultPadding), - InputTextField( + obscureText: false, + focusNode: emailFocusNode, + validator: validateEmail, + ), + const SizedBox(height: Constants.defaultPadding), + SignupFormField( + controller: nameController, + hintText: "Name", + obscureText: false, + focusNode: nameFocusNode, + validator: validateName, + ), + const SizedBox(height: Constants.defaultPadding), + SignupFormField( controller: passwordController, hintText: "Password", - obscureText: true), - const SizedBox(height: Constants.defaultPadding), - InputTextField( + obscureText: true, + focusNode: passwordFocusNode, + validator: validatePassword, + ), + const SizedBox(height: Constants.defaultPadding), + SignupFormField( controller: confirmPasswordController, hintText: "Confirm Password", - obscureText: true), - const SizedBox(height: Constants.defaultPadding), - Container( - width: double.maxFinite, - padding: const EdgeInsets.symmetric( - horizontal: Constants.defaultPadding / 2, vertical: 4), - decoration: BoxDecoration( - color: Constants.secondaryColor, - borderRadius: BorderRadius.circular(3), - border: Border.all( - color: Colors.white, // Set border color here - width: 1, // Set border width here - ), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: accountType, // Initial value - onChanged: null, - items: AccountData.accountTypeMap.keys - .map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - ), - ), - ), - const SizedBox(height: Constants.defaultPadding), - if (accountType != 'Commuter' && names == null) - Container( - width: double.maxFinite, - padding: const EdgeInsets.symmetric( - horizontal: Constants.defaultPadding / 2, - vertical: Constants.defaultPadding + 2.5), - decoration: BoxDecoration( - color: Constants.secondaryColor, - borderRadius: BorderRadius.circular(3), - border: Border.all( - color: Colors.white, // Set border color here - width: 1, // Set border width here - ), - ), - child: const Text( - "Loading Routes...", - style: TextStyle(fontSize: 15), - ), + obscureText: true, + focusNode: confirmPasswordFocusNode, + validator: validateConfirmPassword, ), - if (accountType != 'Commuter' && names != null) + const SizedBox(height: Constants.defaultPadding), Container( width: double.maxFinite, padding: const EdgeInsets.symmetric( @@ -249,16 +298,10 @@ class _SignupFormState extends State { ), child: DropdownButtonHideUnderline( child: DropdownButton( - value: chosenRoute, // Initial value - onChanged: (String? newValue) { - // Handle dropdown value change - if (newValue != null) { - setState(() { - chosenRoute = newValue; - }); - } - }, - items: names!.map>((String value) { + value: accountType, // Initial value + onChanged: null, + items: AccountData.accountTypeMap.keys + .map>((String value) { return DropdownMenuItem( value: value, child: Text(value), @@ -267,32 +310,86 @@ class _SignupFormState extends State { ), ), ), - const SizedBox(height: Constants.defaultPadding * 2), - Button( - onTap: signUserUp, - text: "Sign Up", - ), - const SizedBox(height: Constants.defaultPadding * 2.5), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'Already have an account?', - style: TextStyle(color: Colors.white), - ), - const SizedBox(width: 4), - GestureDetector( - onTap: widget.onTap, + const SizedBox(height: Constants.defaultPadding), + if (accountType != 'Commuter' && names == null) + Container( + width: double.maxFinite, + padding: const EdgeInsets.symmetric( + horizontal: Constants.defaultPadding / 2, + vertical: Constants.defaultPadding + 2.5), + decoration: BoxDecoration( + color: Constants.secondaryColor, + borderRadius: BorderRadius.circular(3), + border: Border.all( + color: Colors.white, // Set border color here + width: 1, // Set border width here + ), + ), child: const Text( - 'Login now', - style: TextStyle( - color: Constants.primaryColor, - fontWeight: FontWeight.bold), + "Loading Routes...", + style: TextStyle(fontSize: 15), + ), + ), + if (accountType != 'Commuter' && names != null) + Container( + width: double.maxFinite, + padding: const EdgeInsets.symmetric( + horizontal: Constants.defaultPadding / 2, vertical: 4), + decoration: BoxDecoration( + color: Constants.secondaryColor, + borderRadius: BorderRadius.circular(3), + border: Border.all( + color: Colors.white, // Set border color here + width: 1, // Set border width here + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: chosenRoute, // Initial value + onChanged: (String? newValue) { + // Handle dropdown value change + if (newValue != null) { + setState(() { + chosenRoute = newValue; + }); + } + }, + items: names!.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), ), - ) - ], - ) - ], + ), + const SizedBox(height: Constants.defaultPadding * 2), + Button( + onTap: signUserUp, + text: "Sign Up", + ), + const SizedBox(height: Constants.defaultPadding * 2.5), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Already have an account?', + style: TextStyle(color: Colors.white), + ), + const SizedBox(width: 4), + GestureDetector( + onTap: widget.onTap, + child: const Text( + 'Login now', + style: TextStyle( + color: Constants.primaryColor, + fontWeight: FontWeight.bold), + ), + ) + ], + ) + ], + ), ), ); } diff --git a/transitrack_web/lib/components/acknowledge_report.dart b/transitrack_web/lib/components/acknowledge_report.dart new file mode 100644 index 0000000..cb1dcb4 --- /dev/null +++ b/transitrack_web/lib/components/acknowledge_report.dart @@ -0,0 +1,104 @@ +import 'package:awesome_dialog/awesome_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:transitrack_web/models/report_model.dart'; +import 'package:transitrack_web/style/constants.dart'; +import 'package:url_launcher/url_launcher.dart'; + +void acknowledgeReport(BuildContext context, ReportData report, + TextEditingController emailController, String routeName) { + // Create a default message + String defaultMessage = 'Dear Reporter,\n\n' + 'We have received your report regarding "${report.getReportType()}" on ${DateFormat('MMM d, y hh:mm a').format(report.timestamp.toDate())}.\n' + 'Plate Number: ${report.report_jeepney}\n' + 'Driver\'s Account: ${report.report_recepient}\n\n' + 'Thank you for bringing this to our attention. We will take the necessary actions.\n\n' + 'Best regards,\n' + '$routeName Jeepney Operator'; + + // Initialize the email controller with the default message + emailController.text = defaultMessage; + + AwesomeDialog( + context: context, + dialogType: DialogType.noHeader, + width: 1500, + body: PointerInterceptor( + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(30.0), + child: Column( + children: [ + TextField( + controller: TextEditingController(text: report.report_sender), + decoration: InputDecoration( + labelText: 'Reporter Email', + border: OutlineInputBorder(), + ), + enabled: false, // Makes the text field uneditable + ), + const SizedBox(height: Constants.defaultPadding / 2), + TextField( + controller: emailController, + maxLines: 12, + decoration: InputDecoration( + hintText: 'Message', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: Constants.defaultPadding / 2), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () async { + String emailBody = emailController.text; + String subject = + '${report.getReportType()} Report Acknowledgement'; + String body = Uri.encodeComponent(emailBody); + + final Uri emailUri = Uri( + scheme: 'https', + host: 'mail.google.com', + path: '/mail/', + queryParameters: { + 'view': 'cm', + 'fs': '1', + 'to': report.report_sender, + 'su': subject, + 'body': emailBody, + }, + ); + + if (await canLaunchUrl(emailUri)) { + await launchUrl(emailUri); + } else { + throw Exception('Could not launch $emailUri'); + } + + Navigator.pop(context); + }, + child: Text('Send'), + ), + ElevatedButton( + onPressed: () { + // Handle cancel action + Navigator.pop(context); + }, + child: Text('Cancel'), + ), + ], + ), + ], + ), + ), + ], + ), + ), + showCloseIcon: true, + dismissOnBackKeyPress: true, + dismissOnTouchOutside: true, + ).show(); +} diff --git a/transitrack_web/lib/components/attach_img_button.dart b/transitrack_web/lib/components/attach_img_button.dart new file mode 100644 index 0000000..2adff99 --- /dev/null +++ b/transitrack_web/lib/components/attach_img_button.dart @@ -0,0 +1,80 @@ +import 'dart:typed_data'; + +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; + +// allows selection of image from a source +pickImage(ImageSource source) async { + final ImagePicker _imagePicker = + ImagePicker(); // creates an instance of ImagePicker plugin + XFile? _file = await _imagePicker.pickImage( + source: source); // opens img picker and waits the user to select an image + if (_file != null) { + return await _file + .readAsBytes(); // if an img is selected it reads and returns Uint8List (a list of bytes) + } + print('No Image Selected'); +} + +final FirebaseStorage _storage = FirebaseStorage + .instance; // create an instance of FirebaseStorage to access its functionality + +// uploads image to FirebaseStorage and return its downloadURL +Future uploadImageToStorage( + String name, Uint8List file, String directory) async { + try { + Reference ref = _storage + .ref() + .child(directory) + .child(name); // get reference to FirebaseStorage location + UploadTask uploadTask = ref.putData(file); // upload img as binary data + TaskSnapshot snapshot = await uploadTask; // waits for upload to complete + String downloadUrl = + await snapshot.ref.getDownloadURL(); // retrive the download url + return downloadUrl; + } catch (e) { + throw Exception('Error uploading image: $e'); + } +} + +// definition of variables of attachment button class +// allows us to create an object for attachment button +// 'final' are immutable variables +class AttachmentButton extends StatelessWidget { + final VoidCallback onPressed; + final String label; + final IconData icon; + final double iconSize; + final double fontSize; + final Color iconColor; + final Color textColor; + // constructor that initializes the object's properties + const AttachmentButton({ + super.key, + required this.onPressed, + required this.label, + required this.icon, + this.iconSize = 14.0, + this.fontSize = 12.0, + this.iconColor = Colors.white, + this.textColor = Colors.white, + }); + + // build(BuildContext context) is a method from StatelessWidget + // @override annotation makes it clear that we are replacing the original method + @override + // build method is required when creating widgets + Widget build(BuildContext context) { + // context provides the widget's location in the widget tree + // we return an elevated button widget with icon for the attachment button widget + return ElevatedButton.icon( + onPressed: onPressed, + icon: Icon(icon, size: iconSize, color: iconColor), + label: Text( + label, + style: TextStyle(fontSize: fontSize, color: textColor), + ), + ); + } +} diff --git a/transitrack_web/lib/components/fare_matrix.dart b/transitrack_web/lib/components/fare_matrix.dart new file mode 100644 index 0000000..9454efb --- /dev/null +++ b/transitrack_web/lib/components/fare_matrix.dart @@ -0,0 +1,334 @@ +import 'package:awesome_dialog/awesome_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:mapbox_gl/mapbox_gl.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:transitrack_web/models/route_model.dart'; +import 'package:transitrack_web/style/constants.dart'; + +import 'package:transitrack_web/services/find_location.dart'; +import '../../services/nearest_index.dart'; +import '../services/calculate_distance.dart'; + +class FareMatrix extends StatefulWidget { + final RouteData route; + const FareMatrix({super.key, required this.route}); + + @override + FareMatrixState createState() => FareMatrixState(); +} + +class FareMatrixState extends State { + late int computedFarePrice = 0; + late int discountedFarePrice = 0; + late double increasePerKm = 0; + double totalDistance = 0.0; + late String selectedFrom; // Default value for "From" + late String selectedTo; // Default value for "To" + late List stops = []; // stop coordinates + late List routeCoordinates = []; + late Map locations = + {}; // Map to store coordinates and addresses + + @override + void initState() { + super.initState(); + + stops = widget.route.stopsCoordinates; + routeCoordinates = widget.route.routeCoordinates; + increasePerKm = widget.route.perKmRate; + + // Populate the locations list for dropdown options + convertCoordinates(); + } + + @override + void didUpdateWidget(covariant FareMatrix oldWidget) { + super.didUpdateWidget(oldWidget); + + // Check if stopsCoordinates has changed + if (widget.route.stopsCoordinates != oldWidget.route.stopsCoordinates) { + setState(() { + stops = widget.route.stopsCoordinates; // Update the stops list + routeCoordinates = + widget.route.routeCoordinates; // Update the route coordinates list + }); + convertCoordinates(); // Refresh the dropdown options + } + } + + bool isLoading = true; + + void convertCoordinates() async { + setState(() { + isLoading = true; + }); + + Map updatedLocations = {}; + + for (LatLng stop in stops) { + String address = + await findAddress(LatLng(stop.latitude, stop.longitude), true); + updatedLocations[stop] = address; + } + + setState(() { + locations = updatedLocations; + isLoading = false; + + if (locations.isNotEmpty) { + selectedFrom = locations.values.first; + selectedTo = locations.values.first; + computeDistance(); + } + }); + } + + void computeDistance() { + totalDistance = 0.0; + // Find the LatLng coordinates for the selected addresses + LatLng? fromCoordinates = locations.entries + .firstWhere((entry) => entry.value == selectedFrom) + .key; + LatLng? toCoordinates = + locations.entries.firstWhere((entry) => entry.value == selectedTo).key; + + // Find the nearest points in the route for the start and end + int indexFrom = findNearestLatLngIndex(fromCoordinates, routeCoordinates); + int indexTo = findNearestLatLngIndex(toCoordinates, routeCoordinates); + // print('From: $indexFrom'); + // print('To: $indexTo'); + + if (widget.route.isClockwise) { + // Traverse the array clockwise + for (int i = indexFrom; + i != indexTo; + i = (i + 1) % routeCoordinates.length) { + // print( + // 'Calculating distance between: ${routeCoordinates[i]} and ${routeCoordinates[(i + 1) % routeCoordinates.length]}'); + totalDistance += calculateDistance( + routeCoordinates[i], + routeCoordinates[(i + 1) % routeCoordinates.length], + ); + } + } else { + // Reverse the array and traverse counterclockwise + for (int i = indexFrom; + i != indexTo; + i = (i - 1 + routeCoordinates.length) % routeCoordinates.length) { + // print( + // 'Calculating distance between: ${routeCoordinates[i]} and ${routeCoordinates[(i - 1 + routeCoordinates.length) % routeCoordinates.length]}'); + totalDistance += calculateDistance( + routeCoordinates[i], + routeCoordinates[ + (i - 1 + routeCoordinates.length) % routeCoordinates.length], + ); + } + } + + print('Total Distance: $totalDistance'); + computeFarePrice(); + } + + void computeFarePrice() { + double baseFare = widget.route.routeFare; // Base fare from the route + double additionalFare = 0; + int totalFare = 0; + + // Round totalDistance based on the decimal part + double roundedDistance = (totalDistance % 1 > 0.8) + ? totalDistance.ceilToDouble() // Round up + : totalDistance.floorToDouble(); // Round down + + // Add +1 for every additional 4 km beyond the first 4 km + if (totalDistance > 4) { + additionalFare = + (roundedDistance - 4) * increasePerKm; // +2 for each 4 km + } + + // Compute the total fare + totalFare = (baseFare + additionalFare).toInt(); + + // Update the state with the computed fare + setState(() { + computedFarePrice = totalFare; + discountedFarePrice = (totalFare * 0.8).ceil(); + }); + // print('Rounded Distance: $roundedDistance'); + // print('Base Fare: $baseFare'); + // print('Additional Fare: $additionalFare'); + // print('Total Fare: $computedFarePrice'); + // print('Discounted Fare: $discountedFarePrice'); + } + + Widget buildDropdown({ + required String label, + required String selectedValue, + required ValueChanged onChanged, + }) { + return DropdownMenu( + label: Text(label), + width: 350, + menuHeight: 250, + enableFilter: true, + requestFocusOnTap: true, + initialSelection: selectedValue, + onSelected: (newValue) { + if (newValue is String) { + onChanged(newValue); + } + }, + dropdownMenuEntries: locations.values.map((String address) { + return DropdownMenuEntry( + value: address, // Use the address as the dropdown value + label: address, // Display the address in the dropdown + ); + }).toList(), + ); + } + + Widget buildDropdowns({required VoidCallback onDropdownChanged}) { + return Column( + children: [ + buildDropdown( + label: 'From', + selectedValue: selectedFrom, + onChanged: (String? newValue) { + setState(() { + selectedFrom = newValue!; + }); + computeDistance(); // Recalculate distance and fare + onDropdownChanged(); // Notify the dialog to rebuild + }, + ), + SizedBox(height: Constants.defaultPadding), + buildDropdown( + label: 'To', + selectedValue: selectedTo, + onChanged: (String? newValue) { + setState(() { + selectedTo = newValue!; + }); + computeDistance(); // Recalculate distance and fare + onDropdownChanged(); // Notify the dialog to rebuild + }, + ), + ], + ); + } + + void showFareMatrix() { + if (isLoading) { + // Show a loading dialog or message if still loading + AwesomeDialog( + context: context, + dialogType: DialogType.noHeader, + width: 450, + alignment: Alignment.topCenter, + body: Center( + child: CircularProgressIndicator(), + ), + ).show(); + return; + } + + // Show the fare matrix dialog when loading is complete + AwesomeDialog( + context: context, + dialogType: DialogType.noHeader, + width: 450, + alignment: Alignment.topCenter, + body: PointerInterceptor( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter dialogSetState) { + return Column( + children: [ + Container( + padding: const EdgeInsets.all(15.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Fare Matrix', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: () { + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ], + ), + SizedBox(height: Constants.defaultPadding / 2), + const Divider(color: Colors.white), + SizedBox(height: Constants.defaultPadding), + buildDropdowns( + onDropdownChanged: () { + // Update the dialog content when dropdown values change + dialogSetState(() {}); + }, + ), + SizedBox(height: Constants.defaultPadding * 2), + Text( + 'Estimated Trip Fare: $computedFarePrice', + style: TextStyle(fontSize: 16), + ), + Text( + 'Discounted Trip Fare: $discountedFarePrice', + style: TextStyle(fontSize: 16), + ), + SizedBox(height: Constants.defaultPadding * 2), + ], + ), + ), + ], + ); + }, + ), + ), + ).show(); + } + + @override + Widget build(BuildContext context) { + return Column(children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Color(widget.route.routeColor) + .withValues(alpha: 0.3), // Dark background with low opacity + padding: const EdgeInsets.symmetric( + vertical: 12.0, horizontal: 16.0), // Button padding + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.defaultPadding), // Rounded corners + ), + ), + onPressed: isLoading + ? null // Disable the button while loading + : () { + showFareMatrix(); + }, + child: isLoading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Text( + 'View Fare Matrix', + style: TextStyle(fontSize: 12.0, color: Colors.white), + ), + ), + ]); + } +} diff --git a/transitrack_web/lib/components/left_drawer/route_list.dart b/transitrack_web/lib/components/left_drawer/route_list.dart index 9aede19..392ef15 100644 --- a/transitrack_web/lib/components/left_drawer/route_list.dart +++ b/transitrack_web/lib/components/left_drawer/route_list.dart @@ -113,40 +113,40 @@ class _RouteListState extends State { ), if (widget.routes == null) const Center(child: CircularProgressIndicator()), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: Constants.defaultPadding), - child: - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Row( - children: [ - Text("Discounted fare"), - IconButton( - visualDensity: VisualDensity.compact, - onPressed: null, - tooltip: - "Discounted Fare includes Student, PWD, and Senior Citizens", - iconSize: 15, - icon: Icon(Icons.question_mark)) - ], - ), - Switch( - activeColor: Colors.blue, - activeTrackColor: Colors.blue.withValues(alpha: 0.5), - inactiveThumbColor: Colors.grey, - value: show_discounted, - onChanged: (value) { - setState(() { - show_discounted = value; - }); + // Padding( + // padding: + // const EdgeInsets.symmetric(horizontal: Constants.defaultPadding), + // child: + // Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + // const Row( + // children: [ + // Text("Discounted fare"), + // IconButton( + // visualDensity: VisualDensity.compact, + // onPressed: null, + // tooltip: + // "Discounted Fare includes Student, PWD, and Senior Citizens", + // iconSize: 15, + // icon: Icon(Icons.question_mark)) + // ], + // ), + // Switch( + // activeColor: Colors.blue, + // activeTrackColor: Colors.blue.withValues(alpha: 0.5), + // inactiveThumbColor: Colors.grey, + // value: show_discounted, + // onChanged: (value) { + // setState(() { + // show_discounted = value; + // }); - if (widget.user != null) { - updateBooleanField(widget.user!.account_id, value); - } - }, - ), - ]), - ), + // if (widget.user != null) { + // updateBooleanField(widget.user!.account_id, value); + // } + // }, + // ), + // ]), + // ), ], ); } diff --git a/transitrack_web/lib/components/left_drawer/route_list_tile.dart b/transitrack_web/lib/components/left_drawer/route_list_tile.dart index 79d4af4..5745c54 100644 --- a/transitrack_web/lib/components/left_drawer/route_list_tile.dart +++ b/transitrack_web/lib/components/left_drawer/route_list_tile.dart @@ -46,10 +46,10 @@ class _RouteListTileState extends State { Text(formatTime(widget.route.routeTime), style: const TextStyle(color: Colors.white54), overflow: TextOverflow.ellipsis), - Text( - "${widget.show_discounted ? widget.route.routeFareDiscounted : widget.route.routeFare} pesos", - style: const TextStyle(color: Colors.white54), - overflow: TextOverflow.ellipsis), + // Text( + // "${widget.show_discounted ? widget.route.perKmRate : widget.route.routeFare} pesos", + // style: const TextStyle(color: Colors.white54), + // overflow: TextOverflow.ellipsis), ], ), selectedTileColor: Colors.white10, diff --git a/transitrack_web/lib/components/map_related/map.dart b/transitrack_web/lib/components/map_related/map.dart index 38e3c47..ec317c2 100644 --- a/transitrack_web/lib/components/map_related/map.dart +++ b/transitrack_web/lib/components/map_related/map.dart @@ -13,6 +13,7 @@ import 'package:transitrack_web/services/mapbox/add_eta_line.dart'; import 'package:transitrack_web/services/mapbox/add_image_from_asset.dart'; import 'package:transitrack_web/services/mapbox/animate_ripple.dart'; import 'package:transitrack_web/services/mapbox/request_location.dart'; +import 'package:transitrack_web/services/mapbox/minute_old_checker.dart'; import '../../config/keys.dart'; import '../../config/map_settings.dart'; @@ -20,6 +21,8 @@ import '../../config/responsive.dart'; import '../../models/account_model.dart'; import '../../models/jeep_model.dart'; import '../../models/route_model.dart'; +import '../../models/ping_model.dart'; + import '../../style/constants.dart'; import '../account_related/route_manager/route_manager_options.dart'; import '../right_panel/desktop_route_info.dart'; @@ -52,17 +55,31 @@ class _MapWidgetState extends State with TickerProviderStateMixin { late List? jeeps; late LatLng? myLocation; late int _configRoute; + late int _configStops; late MapboxMapController _mapController; late StreamSubscription _positionStream; - late Circle? deviceCircle; + late Circle? + deviceCircle; // the white circle showing the passengers current location + bool mapLoaded = false; // used to trigger actions once map is fully loaded + // for route points, lines, and jeepneys late List setRoute; + late List setRouteCopy; + late List setStops; + LatLng? selectedStop; // To store the selected stop's coordinates List circles = []; + List stopsCircles = []; List lines = []; List jeepEntities = []; + // Ping Fetching + StreamSubscription? pingListener; + List pings = []; + late Timer timer; + + // set to true once user allows location service bool gpsTracking = false; JeepEntity? selectedJeep; @@ -75,11 +92,35 @@ class _MapWidgetState extends State with TickerProviderStateMixin { _value = widget.route; jeeps = widget.jeeps; _configRoute = -1; + _configStops = -1; myLocation = null; deviceCircle = null; }); + + // if the map is loaded refresh the available pings + if (mapLoaded) { + refreshPingLayer(); + } + + // runs every second to update the pings displayed on the map + timer = Timer.periodic(const Duration(seconds: 1), (Timer timer) { + // if map is fully loaded and routeData is not null + if (mapLoaded && _value != null) { + // we set pings to those ping's within the 1 minute duration + setState(() { + pings = pings + .where((element) => + minuteOldChecker(element.ping_timestamp.toDate())) + .toList(); + }); + + // converts pings as a GeoJSON format to set as the source for the pings layer + _mapController.setGeoJsonSource("pings", listToGeoJSON(pings)); + } + }); } + // triggers request loc permission and if allowed listen to location void startListening() async { var permission = await requestLocationPermission(context); @@ -93,6 +134,7 @@ class _MapWidgetState extends State with TickerProviderStateMixin { } @override + // if the user selects a new route update the map information void didUpdateWidget(covariant MapWidget oldWidget) { super.didUpdateWidget(oldWidget); // if route choice changed @@ -112,8 +154,11 @@ class _MapWidgetState extends State with TickerProviderStateMixin { _value = widget.route; }); + // route lines and pings update addLine(); + refreshPingLayer(); + // clear jeepney entities in the map _mapController.clearSymbols().then((value) => jeepEntities.clear()); } @@ -127,6 +172,7 @@ class _MapWidgetState extends State with TickerProviderStateMixin { } } + // update jeepney entities void updateJeeps() { List? toUpdate = jeeps; if (toUpdate != null && toUpdate.isNotEmpty) { @@ -199,6 +245,7 @@ class _MapWidgetState extends State with TickerProviderStateMixin { } } + // animate the users location indicator circle void animateCircleMovement(LatLng from, LatLng to, Circle circle, TickerProvider tick, MapboxMapController mapController) { final animationController = AnimationController( @@ -224,10 +271,13 @@ class _MapWidgetState extends State with TickerProviderStateMixin { animationController.forward(); } + // sets the map controller when the Mapbox map is created + // controller - instance of a class that allows you to perform operations void _onMapCreated(MapboxMapController controller) { _mapController = controller; } + // check and update user location circle within the map void _listenToDeviceLocation() { _positionStream = Geolocator.getPositionStream( locationSettings: const LocationSettings( @@ -238,6 +288,40 @@ class _MapWidgetState extends State with TickerProviderStateMixin { }); } + // checks if there are new pings available + void listenToPingsFirestore() { + pingListener = FirebaseFirestore.instance + .collection('pings') + .where('ping_route', isEqualTo: _value!.routeId) + .snapshots() + .listen((QuerySnapshot snapshot) { + if (snapshot.docs.isNotEmpty) { + setState(() { + pings = snapshot.docs + .map((doc) => PingData.fromFirestore(doc)) + .where((element) => + minuteOldChecker(element.ping_timestamp.toDate())) + .toList(); + }); + _mapController.setGeoJsonSource("pings", listToGeoJSON(pings)); + } + }); + } + + // update pings within the map + void refreshPingLayer() { + if (_value != null) { + addGeojsonCluster(_mapController, _value!); + listenToPingsFirestore(); + } else { + pingListener?.cancel(); + pings.clear(); + // updates the pings layer of the map to display the current pings listed in GeoJSON format + _mapController.setGeoJsonSource("pings", listToGeoJSON(pings)); + } + } + + // update the user's location indicator void _updateDeviceCircle(LatLng latLng) { setState(() { myLocation = latLng; @@ -276,7 +360,8 @@ class _MapWidgetState extends State with TickerProviderStateMixin { }); } - void update() async { + // update route + void update(String collectionName, List coordinates) async { // show loading circle showDialog( context: context, @@ -284,9 +369,10 @@ class _MapWidgetState extends State with TickerProviderStateMixin { return const Center(child: CircularProgressIndicator()); }); + // updates route coordinates try { Map newAccountSettings = { - 'route_coordinates': setRoute + collectionName: coordinates .map((latLng) => GeoPoint(latLng.latitude, latLng.longitude)) .toList() }; @@ -303,6 +389,7 @@ class _MapWidgetState extends State with TickerProviderStateMixin { } } + // initializes the lines of the map for the route void addLine() { _mapController.clearLines().then((value) => lines.clear()); if (widget.route != null) { @@ -340,27 +427,36 @@ class _MapWidgetState extends State with TickerProviderStateMixin { } } - void addPoints() { - for (var circle in circles) { + // initializes the points on the map + void addPoints(List circlePoints, List coords, int config) { + // make circles and coord array parameters + // remove the coordinate circles within the map + for (var circle in circlePoints) { _mapController.removeCircle(circle); } - circles.clear(); - for (int i = 0; i < setRoute.length; i++) { + // clear the array containing the coordinate circles + circlePoints.clear(); + + // for each route coordinate, add a circle coordinate and add it in the circle coordinates array + for (int i = 0; i < coords.length; i++) { _mapController .addCircle(CircleOptions( circleRadius: 8.0, circleStrokeWidth: 2.0, circleStrokeOpacity: 1, circleColor: intToHexColor(widget.route!.routeColor), - geometry: setRoute[i], + geometry: coords[i], circleStrokeColor: '#FFFFFF', - draggable: _configRoute == 0 ? true : false)) - .then((circle) => circles.add(circle)); + draggable: config == 0 ? true : false)) + .then((circle) => circlePoints.add(circle)); } } + // when line on the map is tapped it adds another coordinate + // it clears the coordinates and reinitialize them again along with the lines void onLineTapped(Line pressedLine) { + // add bool to reuse this funct and use circles and coord array a parameter to reuse int index = lines.indexWhere((line) => pressedLine == line); double x = (pressedLine.options.geometry![0].latitude + @@ -376,23 +472,78 @@ class _MapWidgetState extends State with TickerProviderStateMixin { _mapController.removeCircle(circle); } circles.clear(); - addPoints(); + addPoints(circles, setRoute, _configRoute); _mapController .clearLines() .then((value) => lines.clear()) .then((value) => addLine()); } + // if a coordinate is tapped it would be removed + // it will call add points to reinitialize all points again + // it will call add line to reinitilize all lines again void onCircleTapped(Circle pressedCircle) { + // make circles and coord array a paramter int index = circles.indexWhere((circle) => pressedCircle == circle); if (index != -1) { setRoute.removeAt(index); - addPoints(); + addPoints(circles, setRoute, _configRoute); addLine(); } } + // add stop points within the map + void onStopLocationTapped(LatLng tappedLocation) { + // Add the tapped location to the stops list + setStops.add(tappedLocation); + + // Update the selected stop + setState(() { + selectedStop = tappedLocation; + }); + // Remove existing stop circles from the map + for (var circle in stopsCircles) { + _mapController.removeCircle(circle); + } + stopsCircles.clear(); + + // Reinitialize the stop circles on the map + addPoints(stopsCircles, setStops, _configStops); + } + + void onStopLocationCircleTapped(Circle pressedCircle) { + // Find the index of the tapped stop circle + int index = stopsCircles.indexWhere((circle) => pressedCircle == circle); + + // If the circle exists in the stopsCircles list + if (index != -1) { + // Update the selected stop + setState(() { + selectedStop = stopsCircles[index].options.geometry; + }); + // Remove the stop location from the setStops list + setStops.removeAt(index); + + // Reinitialize the stop circles on the map + addPoints(stopsCircles, setStops, _configStops); + } + } + + void onStopLocationCircleSelected(Circle pressedCircle) { + // Find the index of the tapped stop circle + int index = stopsCircles.indexWhere((circle) => pressedCircle == circle); + + // If the circle exists in the stopsCircles list + if (index != -1) { + // Update the selected stop + setState(() { + selectedStop = stopsCircles[index].options.geometry; + }); + } + } + + // jeep selection on the map void onJeepTapped(Symbol pressedJeep) { if (selectedJeep != null) { if (pressedJeep != selectedJeep!.jeepSymbol) { @@ -468,6 +619,7 @@ class _MapWidgetState extends State with TickerProviderStateMixin { onMapCreated: (controller) { _onMapCreated(controller); }, + // allows you to perform additional setup and customization of the map once the style has been fully loaded onStyleLoadedCallback: () { addETALayer(_mapController); addImageFromAsset(_mapController); @@ -475,7 +627,11 @@ class _MapWidgetState extends State with TickerProviderStateMixin { _mapController.setSymbolTextAllowOverlap(true); _mapController.setSymbolIconIgnorePlacement(true); _mapController.setSymbolTextIgnorePlacement(true); + refreshPingLayer(); widget.mapLoaded(true); + setState(() { + mapLoaded = true; + }); startListening(); }, initialCameraPosition: CameraPosition( @@ -486,9 +642,13 @@ class _MapWidgetState extends State with TickerProviderStateMixin { if (_configRoute == 1) { setRoute.add(latLng); - addPoints(); + addPoints(circles, setRoute, _configRoute); addLine(); } + if (_configStops == 1) { + // Add a new stop location + onStopLocationTapped(latLng); + } }, ), ), @@ -652,6 +812,7 @@ class _MapWidgetState extends State with TickerProviderStateMixin { mapStartZoom)); }, coordConfig: (int coordConfig) { + // we save what is the previous mode int prev = _configRoute; setState(() { @@ -660,58 +821,225 @@ class _MapWidgetState extends State with TickerProviderStateMixin { // unselected any of the choices if (_configRoute < 0) { + print("prev mode: $prev"); + print("configRoute mode: $_configRoute"); // Coming from moving points, we save the new coordinates. if (prev == 0) { - setRoute.clear(); - for (var circle in circles) { - setRoute.add(circle.options.geometry!); - } - - for (var circle in circles) { - _mapController.removeCircle(circle); - } - circles.clear(); if (_configRoute == -1) { - update(); - } - addLine(); - } else if (prev == 1) { - _mapController.onCircleTapped - .remove(onCircleTapped); - _mapController.onLineTapped - .remove(onLineTapped); - - for (var circle in circles) { - _mapController.removeCircle(circle); + // clear the array containing route coordinates + setRoute.clear(); + + // repopulate the array with the new coordinates + for (var circle in circles) { + setRoute.add(circle.options.geometry!); + } + // remove the coordinate circles in the map + for (var circle in circles) { + _mapController.removeCircle(circle); + } + + // clear the array containing coordinate circles + circles.clear(); + // re-initialize the route lines + addLine(); + // Update Firestore + update('route_coordinates', setRoute); + } else if (_configRoute == -2) { + // remove the coordinate circles in the map + for (var circle in circles) { + _mapController.removeCircle(circle); + } + + // clear the array containing coordinate circles + circles.clear(); + // re-initialize the route lines + addLine(); } - circles.clear(); + // if the mode is previously add/delete state + } else if (prev == 1) { if (_configRoute == -1) { - update(); + // remove onTap listeners to ensure that tapping on the map would no longer add/delete points + _mapController.onCircleTapped + .remove(onCircleTapped); + _mapController.onLineTapped + .remove(onLineTapped); + // remove the coordinate circles in the map + for (var circle in circles) { + _mapController.removeCircle(circle); + } + // clear the array containing coordinate circles + circles.clear(); + update('route_coordinates', setRoute); + } else if (_configRoute == -2) { + _mapController.onCircleTapped + .remove(onCircleTapped); + _mapController.onLineTapped + .remove(onLineTapped); + + // Revert to the original route coordinates from the database + setRoute.clear(); + setRoute = List.from(widget + .route!.routeCoordinates); // Deep copy + + // Remove the coordinate circles in the map + for (var circle in circles) { + _mapController.removeCircle(circle); + } + + // Clear the array containing coordinate circles + circles.clear(); + + // Re-initialize the route lines + addLine(); } } + + // if not at default mode } else { - setRoute = widget.route!.routeCoordinates; + // create a deep copy, otherwise the routeCoordinates would also be modified + setRoute = List.from( + widget.route!.routeCoordinates); - // Move points + // Move points mode if (_configRoute == 0) { _mapController.clearLines(); - addPoints(); + // re-initialize the coordinates in the map + addPoints(circles, setRoute, _configRoute); } // Add or Remove Points else if (_configRoute == 1) { + // add the on tap listeners _mapController.onLineTapped.add(onLineTapped); _mapController.onCircleTapped .add(onCircleTapped); - addPoints(); + // re-initialize the coordinates in the map + addPoints(circles, setRoute, _configRoute); } } + }, + stopsConfig: (int stopsConfig) { + // we save what is the previous mode + int prev = _configStops; + + // update _configStops to trigger state change + // note that coordConfig is the variable modified in route_settings + // and _configStops manages mode changes in the map + setState(() { + _configStops = stopsConfig; + }); + + // if at default mode + if (_configStops < 0) { + // Set selectedStop to null + setState(() { + selectedStop = null; + }); + + // if from moving points mode + if (prev == 0) { + // if changes are saved + if (_configStops == -1) { + // clear the array containing route coordinates + setStops.clear(); + + // repopulate the array with the new coordinates + for (var circle in stopsCircles) { + setStops.add(circle.options.geometry!); + } + + // Update the route's stopsCoordinates + setState(() { + widget.route!.stopsCoordinates = setStops; + }); + + // remove the coordinate circles in the map + for (var circle in stopsCircles) { + _mapController.removeCircle(circle); + } + + // clear the array containing coordinate circles + stopsCircles.clear(); + // re-initialize the route lines + addLine(); + // Update Firestore + update('stops_coordinates', setStops); + // if changes are not saved + } else if (_configStops == -2) { + // remove the coordinate circles in the map + for (var circle in stopsCircles) { + _mapController.removeCircle(circle); + } + + // clear the array containing coordinate circles + stopsCircles.clear(); + // re-initialize the route lines + addLine(); + } - if (_configRoute == -2) { - _configRoute = -1; + // if from adding/deleting points mode + } else if (prev == 1) { + // if changes are saved + if (_configStops == -1) { + // remove onTap listeners to ensure that tapping on the map would no longer add/delete points + _mapController.onCircleTapped + .remove(onStopLocationCircleTapped); + // remove the coordinate circles in the map + for (var circle in stopsCircles) { + _mapController.removeCircle(circle); + } + // clear the array containing coordinate circles + stopsCircles.clear(); + // Update the route's stopsCoordinates + setState(() { + widget.route!.stopsCoordinates = setStops; + }); + update('stops_coordinates', setStops); + // if changes are not saved + } else if (_configStops == -2) { + _mapController.onCircleTapped + .remove(onStopLocationCircleTapped); + + // Revert to the original route coordinates from the database + setStops.clear(); + setStops = List.from(widget + .route!.stopsCoordinates); // Deep copy + + // Remove the coordinate circles in the map + for (var circle in stopsCircles) { + _mapController.removeCircle(circle); + } + + // Clear the array containing coordinate circles + stopsCircles.clear(); + + // Re-initialize the route lines + addLine(); + } + } + } else { + // create a deep copy, otherwise the routeCoordinates would also be modified + setStops = List.from( + widget.route!.stopsCoordinates); + // if at adding/deleting points mode + if (_configStops == 0) { + // Add the onCircleTapped listener for selecting stops + _mapController.onCircleTapped + .add(onStopLocationCircleSelected); + addPoints( + stopsCircles, setStops, _configStops); + + // if at moving points mode + } else if (_configStops == 1) { + _mapController.onCircleTapped + .add(onStopLocationCircleTapped); + addPoints( + stopsCircles, setStops, _configStops); + } } }, + selectedStop: selectedStop, )) ], )) diff --git a/transitrack_web/lib/components/right_panel/commuter_feedback_tab.dart b/transitrack_web/lib/components/right_panel/commuter_feedback_tab.dart new file mode 100644 index 0000000..4be3cbc --- /dev/null +++ b/transitrack_web/lib/components/right_panel/commuter_feedback_tab.dart @@ -0,0 +1,395 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:transitrack_web/models/feedback_model.dart'; +import 'package:transitrack_web/models/report_model.dart'; +import 'package:transitrack_web/models/route_model.dart'; +import 'package:transitrack_web/style/constants.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:awesome_dialog/awesome_dialog.dart'; +import 'package:transitrack_web/services/find_location.dart'; +import 'package:mapbox_gl/mapbox_gl.dart'; + +// This widget is called when user selects the feedback/report button + +class CommuterFeedbackTab extends StatelessWidget { + final RouteData route; + final FeedbackData feedBack; + final Function loadCommuters; + const CommuterFeedbackTab( + {super.key, + required this.route, + required this.feedBack, + required this.loadCommuters}); + + // get the name of the driver/feedback recipient + Future getAccountName(String email) async { + try { + var querySnapshot = await FirebaseFirestore.instance + .collection('accounts') + .where('account_email', isEqualTo: email) + .limit(1) + .get(); + + if (querySnapshot.docs.isNotEmpty) { + return querySnapshot.docs.first['account_name'] ?? 'Unknown'; + } + } catch (e) { + debugPrint("Error fetching account name: $e"); + } + return 'Unknown'; + } + + Future deleteFeedback(String senderEmail, Timestamp timestamp) async { + try { + // Query the Firestore collection to find feedback documents + QuerySnapshot querySnapshot = await FirebaseFirestore.instance + .collection('feedbacks') + .where('feedback_sender', + isEqualTo: senderEmail) // Filter by sender email + .where('timestamp', isEqualTo: timestamp) // Filter by timestamp + .limit(1) // Limit the query to 1 document + .get(); // Retrieve the matching document + + // Check if any document was found. + if (querySnapshot.docs.isNotEmpty) { + // Get the first document and delete it. + await querySnapshot.docs.first.reference.delete(); + + // Print success message in the console. + // print("Feedback deleted successfully."); + } else { + // Print a message if no matching feedback is found. + // print("No matching feedback found to delete."); + } + } catch (error) { + // Print an error message if something goes wrong. + // print("Error deleting feedback: $error"); + } + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: getAccountName(feedBack.feedback_recepient), + builder: (context, snapshot) { + String accountName = snapshot.data ?? 'Loading...'; + + return Container( + padding: const EdgeInsets.all(Constants.defaultPadding), + decoration: BoxDecoration( + border: Border.all(color: Colors.white, width: 2), + borderRadius: BorderRadius.circular(Constants.defaultPadding), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(DateFormat('MMMM d, y') + .format(feedBack.timestamp.toDate())), + Text( + DateFormat('hh:mm a') + .format(feedBack.timestamp.toDate()), + style: TextStyle( + fontSize: 13, + color: Colors.white.withValues(alpha: 0.5))), + ], + ), + TextButton.icon( + onPressed: () { + AwesomeDialog( + context: context, + width: 400, + dialogType: DialogType.warning, + padding: const EdgeInsets.all(Constants.defaultPadding), + desc: + "You are about to delete this feedback. This action cannot be undone.", + btnOkText: "Delete", + btnOkColor: Colors.red[600], + btnCancelText: "Cancel", + btnCancelColor: Constants.bgColor, + btnCancelOnPress: () {}, + btnOkOnPress: () async { + await deleteFeedback( + feedBack.feedback_sender, feedBack.timestamp); + + await AwesomeDialog( + context: context, + width: 150, + padding: const EdgeInsets.only( + bottom: Constants.defaultPadding), + dialogType: DialogType.noHeader, + body: CircularProgressIndicator( + color: Color(route.routeColor)), + dismissOnBackKeyPress: false, + dismissOnTouchOutside: false, + autoHide: const Duration(milliseconds: 1000)) + .show(); + loadCommuters(); + }, + ).show(); + }, + icon: Icon(Icons.delete, color: Colors.red[600]), + label: Text("Delete", + style: TextStyle(color: Colors.red[600])), + ) + ], + ), + const Divider(color: Colors.white), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(feedBack.feedback_jeepney), + Text(accountName), // Display the fetched account name + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: List.generate(5, (index) { + bool enabled = index < feedBack.feedback_jeepney_rating; + return Icon( + enabled ? Icons.star : Icons.star_border, + color: + enabled ? Color(route.routeColor) : Colors.grey, + ); + })), + Row( + children: List.generate(5, (index) { + bool enabled = index < feedBack.feedback_driving_rating; + return Icon( + enabled ? Icons.star : Icons.star_border, + color: + enabled ? Color(route.routeColor) : Colors.grey, + ); + })), + ], + ), + ], + ), + const Divider(color: Colors.white), + SizedBox( + height: Constants.defaultPadding * 2.5, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Text(feedBack.feedback_content), + ), + ), + if (feedBack.feedback_img != null && + feedBack.feedback_img!.isNotEmpty) + SizedBox( + height: 200, + child: Image.network( + feedBack.feedback_img!, + fit: BoxFit.contain, + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Text( + 'Failed to load image', + style: TextStyle(color: Colors.red), + ); + }, + ), + ), + ], + ), + ); + }, + ); + } +} + +class CommuterReportTab extends StatelessWidget { + final RouteData route; + final ReportData report; + final Function loadCommuters; + const CommuterReportTab({ + super.key, + required this.route, + required this.report, + required this.loadCommuters, + }); + + // get the name of the driver/feedback recipient + Future getAccountName(String email) async { + try { + var querySnapshot = await FirebaseFirestore.instance + .collection('accounts') + .where('account_email', isEqualTo: email) + .limit(1) + .get(); + + if (querySnapshot.docs.isNotEmpty) { + return querySnapshot.docs.first['account_name'] ?? 'Unknown'; + } + } catch (e) { + debugPrint("Error fetching account name: $e"); + } + return 'Unknown'; + } + + Future deleteReport(String senderEmail, Timestamp timestamp) async { + try { + QuerySnapshot querySnapshot = await FirebaseFirestore.instance + .collection('reports') + .where('report_sender', isEqualTo: senderEmail) + .where('timestamp', isEqualTo: timestamp) + .limit(1) + .get(); + + if (querySnapshot.docs.isNotEmpty) { + await querySnapshot.docs.first.reference.delete(); + } + } catch (error) { + debugPrint("Error deleting report: $error"); + } + } + + Future getAddressFromCoordinates( + double latitude, double longitude) async { + return await findAddress(LatLng(latitude, longitude), false); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: Future.wait([ + getAccountName(report.report_recepient), + getAddressFromCoordinates( + report.report_location.latitude, report.report_location.longitude), + ]), + builder: (context, snapshot) { + String accountName = snapshot.data?[0] ?? 'Loading...'; + String address = snapshot.data?[1] ?? 'Fetching address...'; + + return Container( + padding: const EdgeInsets.all(Constants.defaultPadding), + decoration: BoxDecoration( + border: Border.all(color: Colors.white, width: 2), + borderRadius: BorderRadius.circular(Constants.defaultPadding), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(DateFormat('MMMM d, y') + .format(report.timestamp.toDate())), + Text( + DateFormat('hh:mm a') + .format(report.timestamp.toDate()), + style: TextStyle( + fontSize: 13, + color: Colors.white.withValues(alpha: 0.5))), + ], + ), + TextButton.icon( + onPressed: () { + AwesomeDialog( + context: context, + width: 400, + dialogType: DialogType.warning, + padding: const EdgeInsets.all(Constants.defaultPadding), + desc: + "You are about to delete this report. This action cannot be undone.", + btnOkText: "Delete", + btnOkColor: Colors.red[600], + btnCancelText: "Cancel", + btnCancelColor: Constants.bgColor, + btnCancelOnPress: () {}, + btnOkOnPress: () async { + await deleteReport( + report.report_sender, report.timestamp); + + await AwesomeDialog( + context: context, + width: 150, + padding: const EdgeInsets.only( + bottom: Constants.defaultPadding), + dialogType: DialogType.noHeader, + body: CircularProgressIndicator( + color: Color(route.routeColor)), + dismissOnBackKeyPress: false, + dismissOnTouchOutside: false, + autoHide: const Duration(milliseconds: 1000)) + .show(); + loadCommuters(); + }, + ).show(); + }, + icon: Icon(Icons.delete, color: Colors.red[600]), + label: Text("Delete", + style: TextStyle(color: Colors.red[600])), + ) + ], + ), + const Divider(color: Colors.white), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(ReportData.reportDetails[report.report_type].reportType), + Text(address), + ], + ), + const Divider(color: Colors.white), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(accountName), + Text( + report.report_recepient, + style: TextStyle( + fontSize: 13, + color: Colors.white.withValues(alpha: 0.5)), + ), + ], + ), + Text(report.report_jeepney), + ], + ), + const Divider(color: Colors.white), + SizedBox( + height: Constants.defaultPadding * 2.5, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Text(report.report_content), + ), + ), + if (report.report_img != null && report.report_img!.isNotEmpty) + SizedBox( + height: 200, + child: Image.network( + report.report_img!, + fit: BoxFit.contain, + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Text( + 'Failed to load image', + style: TextStyle(color: Colors.red), + ); + }, + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/transitrack_web/lib/components/right_panel/desktop_route_info.dart b/transitrack_web/lib/components/right_panel/desktop_route_info.dart index 1c92afb..c0f138f 100644 --- a/transitrack_web/lib/components/right_panel/desktop_route_info.dart +++ b/transitrack_web/lib/components/right_panel/desktop_route_info.dart @@ -15,6 +15,7 @@ import '../select_jeep_prompt.dart'; import '../../components/cooldown_button.dart'; import '../../services/send_ping.dart'; +import '../../components/fare_matrix.dart'; // This widget displays all the information for the route for the desktop view class DesktopRouteInfo extends StatefulWidget { @@ -175,24 +176,25 @@ class _DesktopRouteInfoState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 5.5), - child: Text( - _value.routeName, - style: - const TextStyle(fontSize: 20, fontWeight: FontWeight.w500), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - )), + // Expanded( + // child: Padding( + // padding: const EdgeInsets.symmetric(vertical: 5.5), + // child: Text( + // _value.routeName, + // style: + // const TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // ), + // )), + FareMatrix(route: widget.route), const SizedBox(width: Constants.defaultPadding / 2), if (widget.user != null) Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text("Wait a Ride", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 10)), + // const Text("Wait a Ride", + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // style: TextStyle(fontSize: 10)), const SizedBox(width: Constants.defaultPadding / 3), if (_gpsPermission) CooldownButton( diff --git a/transitrack_web/lib/components/right_panel/feedback_form.dart b/transitrack_web/lib/components/right_panel/feedback_form.dart index 2ecae1b..58566fb 100644 --- a/transitrack_web/lib/components/right_panel/feedback_form.dart +++ b/transitrack_web/lib/components/right_panel/feedback_form.dart @@ -1,5 +1,8 @@ +import 'dart:typed_data'; + import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; import '../../models/account_model.dart'; import '../../models/jeep_model.dart'; @@ -7,6 +10,7 @@ import '../../models/route_model.dart'; import '../../style/constants.dart'; import '../../style/style.dart'; import '../button.dart'; +import '../attach_img_button.dart'; import '../text_field.dart'; // This widget is called when user selects the feedback button @@ -27,6 +31,7 @@ class _FeedbackFormState extends State { int _drivingRating = 0; int _jeepRating = 0; + Uint8List? _image; void sendFeedback() async { // show loading circle @@ -41,6 +46,14 @@ class _FeedbackFormState extends State { if (_drivingRating + _jeepRating > 0) { if (widget.user!.account_email != widget.jeep.driver!.account_email) { try { + String? imageUrl; // nullable string variable for url + if (_image != null) { + imageUrl = await uploadImageToStorage( + '${widget.user!.account_email}_${DateTime.now().millisecondsSinceEpoch}', + _image!, + 'feedback_images'); + } + // Add a new document with auto-generated ID await FirebaseFirestore.instance .collection('feedbacks') @@ -52,7 +65,8 @@ class _FeedbackFormState extends State { 'feedback_content': feedBackController.text, 'feedback_driving_rating': _drivingRating, 'feedback_jeepney_rating': _jeepRating, - 'feedback_route': widget.route.routeId + 'feedback_route': widget.route.routeId, + 'feedback_img': imageUrl, }) .then((value) => Navigator.pop(context)) .then((value) => Navigator.pop(context)); @@ -99,6 +113,17 @@ class _FeedbackFormState extends State { }); } + // sets the Uint8List variable to be the selected image file + // needs to be within the widget + void selectImage() async { + Uint8List img = await pickImage(ImageSource.gallery); + // setState is used to notify the framework that the internal state of the widget has changed + // signals that there is a need to rebuild + setState(() { + _image = img; + }); + } + @override Widget build(BuildContext context) { return Padding( @@ -211,12 +236,36 @@ class _FeedbackFormState extends State { const Divider(color: Colors.white), const SizedBox(height: Constants.defaultPadding), InputTextField( - controller: feedBackController, - hintText: "Feedback", - obscureText: false, - lines: 4, - limit: 150), + controller: feedBackController, + hintText: "Feedback", + obscureText: false, + lines: 4, + limit: 150, + helperWidget: AttachmentButton( + onPressed: selectImage, + label: "Attach Image", + icon: Icons.add_a_photo, + ), + ), const SizedBox(height: Constants.defaultPadding), + if (_image != null) + Column( + children: [ + const Text( + "Selected Image:", + style: TextStyle(color: Colors.white), + ), + const SizedBox(height: Constants.defaultPadding / 2), + IntrinsicHeight( + // Image.memory obtains image from Uint8List + child: Image.memory( + _image!, + fit: BoxFit.contain, + ), + ), + const SizedBox(height: Constants.defaultPadding), + ], + ), Button( onTap: () => sendFeedback(), text: _drivingRating + _jeepRating == 0 diff --git a/transitrack_web/lib/components/right_panel/feedback_tab.dart b/transitrack_web/lib/components/right_panel/feedback_tab.dart index 862b33a..256898f 100644 --- a/transitrack_web/lib/components/right_panel/feedback_tab.dart +++ b/transitrack_web/lib/components/right_panel/feedback_tab.dart @@ -75,13 +75,27 @@ class FeedbackTab extends StatelessWidget { ], ), const Divider(color: Colors.white), - const SizedBox(height: Constants.defaultPadding), SizedBox( - height: Constants.defaultPadding * 1.5, + height: Constants.defaultPadding * 2.5, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: Text(feedBack.feedback_content)), - ) + ), + if (feedBack.feedback_img != null && + feedBack.feedback_img!.isNotEmpty) + SizedBox( + height: 200, + child: Image.network( + feedBack.feedback_img!, + fit: BoxFit.contain, + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Text( + 'Failed to load image', + style: TextStyle(color: Colors.red), + ); + }, + )), ], ), ); diff --git a/transitrack_web/lib/components/right_panel/feedback_viewer.dart b/transitrack_web/lib/components/right_panel/feedback_viewer.dart index 0548c4d..cc507d5 100644 --- a/transitrack_web/lib/components/right_panel/feedback_viewer.dart +++ b/transitrack_web/lib/components/right_panel/feedback_viewer.dart @@ -109,8 +109,7 @@ class _FeedBackViewerState extends State { if (widget.feedbacks.isNotEmpty && routes.isNotEmpty) Column( children: [ - SizedBox( - height: 200, + IntrinsicHeight( child: FeedbackTab( route: routes[widget.feedbacks[index].feedback_route], isDriver: widget.isDriver, diff --git a/transitrack_web/lib/components/right_panel/mobile_route_info.dart b/transitrack_web/lib/components/right_panel/mobile_route_info.dart index b3ad111..2b6e97c 100644 --- a/transitrack_web/lib/components/right_panel/mobile_route_info.dart +++ b/transitrack_web/lib/components/right_panel/mobile_route_info.dart @@ -16,6 +16,8 @@ import 'selected_jeep_info.dart'; import '../cooldown_button.dart'; import '../../services/send_ping.dart'; +import '../../components/fare_matrix.dart'; + // This widget displays all the information for the route for the mobile view class MobileRouteInfo extends StatefulWidget { @@ -302,22 +304,23 @@ class _MobileRouteInfoState extends State { crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.start, children: [ - Text( - "${_value.routeFare} Regular", - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Text( - "${_value.routeFareDiscounted} Discounted", - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + // Text( + // "${_value.routeFare} Regular", + // style: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.w500), + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // ), + // Text( + // "${_value.perKmRate} Discounted", + // style: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.w500), + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // ), + FareMatrix(route: widget.route), Text( formatTime(_value.routeTime), style: const TextStyle( diff --git a/transitrack_web/lib/components/right_panel/report_form.dart b/transitrack_web/lib/components/right_panel/report_form.dart index 07fdb03..dcfd959 100644 --- a/transitrack_web/lib/components/right_panel/report_form.dart +++ b/transitrack_web/lib/components/right_panel/report_form.dart @@ -1,5 +1,8 @@ +import 'dart:typed_data'; + import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:mapbox_gl/mapbox_gl.dart'; import 'package:transitrack_web/services/find_location.dart'; @@ -10,6 +13,7 @@ import '../../models/route_model.dart'; import '../../style/constants.dart'; import '../../style/style.dart'; import '../button.dart'; +import '../attach_img_button.dart'; import '../text_field.dart'; // This widget is called when user selects the report button @@ -30,6 +34,7 @@ class _ReportFormState extends State { String reportType = "Lost Item"; late String address; + Uint8List? _image; @override void initState() { @@ -38,8 +43,10 @@ class _ReportFormState extends State { } void getAddress() async { - String result = await findAddress(LatLng(widget.jeep.jeep.location.latitude, - widget.jeep.jeep.location.longitude)); + String result = await findAddress( + LatLng(widget.jeep.jeep.location.latitude, + widget.jeep.jeep.location.longitude), + false); setState(() { address = result; }); @@ -56,6 +63,13 @@ class _ReportFormState extends State { // report field is not empty if (reportController.text.isNotEmpty) { try { + String? imageUrl; + if (_image != null) { + imageUrl = await uploadImageToStorage( + '${widget.user!.account_email}_${DateTime.now().millisecondsSinceEpoch}', + _image!, + 'report_images'); + } // Add a new document with auto-generated ID await FirebaseFirestore.instance .collection('reports') @@ -68,7 +82,8 @@ class _ReportFormState extends State { 'report_type': ReportData.reportTypeMap[reportType], 'report_location': GeoPoint(widget.jeep.jeep.location.latitude, widget.jeep.jeep.location.longitude), - 'report_route': widget.route.routeId + 'report_route': widget.route.routeId, + 'report_img': imageUrl, }) .then((value) => Navigator.pop(context)) .then((value) => Navigator.pop(context)); @@ -89,6 +104,17 @@ class _ReportFormState extends State { // try sign up } + // sets the Uint8List variable to be the selected image file + // needs to be within the widget + void selectImage() async { + Uint8List img = await pickImage(ImageSource.gallery); + // setState is used to notify the framework that the internal state of the widget has changed + // signals that there is a need to rebuild + setState(() { + _image = img; + }); + } + void errorMessage(String message) { showDialog( context: context, @@ -215,8 +241,30 @@ class _ReportFormState extends State { hintText: "Report", obscureText: false, lines: 4, - limit: 150), + limit: 150, + helperWidget: AttachmentButton( + onPressed: selectImage, + label: "Attach Image", + icon: Icons.add_a_photo, + )), const SizedBox(height: Constants.defaultPadding), + if (_image != null) + Column( + children: [ + const Text( + "Selected Image:", + style: TextStyle(color: Colors.white), + ), + const SizedBox(height: Constants.defaultPadding / 2), + IntrinsicHeight( + child: Image.memory( + _image!, + fit: BoxFit.contain, + ), + ), + const SizedBox(height: Constants.defaultPadding), + ], + ), Button( onTap: () => sendReport(), text: "Send Report", diff --git a/transitrack_web/lib/components/right_panel/selected_jeep_info.dart b/transitrack_web/lib/components/right_panel/selected_jeep_info.dart index 42922c0..2d4648f 100644 --- a/transitrack_web/lib/components/right_panel/selected_jeep_info.dart +++ b/transitrack_web/lib/components/right_panel/selected_jeep_info.dart @@ -14,6 +14,8 @@ import '../../config/responsive.dart'; import '../../style/constants.dart'; import 'feedback_form.dart'; +import 'package:flutter/services.dart'; + // This widget displays relevant information of the PUV class SelectedJeepInfo extends StatefulWidget { @@ -187,30 +189,126 @@ class SelectedJeepInfoBox extends StatefulWidget { } class _SelectedJeepInfoBoxState extends State { + bool isSharing = false; + String? currentShareDocId; + + final FirebaseFirestore firestore = FirebaseFirestore.instance; + + @override + void initState() { + super.initState(); + resetSharingStatus(); + loadSharingStatus(); + } + + @override + void didUpdateWidget(covariant SelectedJeepInfoBox oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.jeep.device_id != widget.jeep.device_id) { + resetSharingStatus(); + loadSharingStatus(); + } + } + + void _toggleSharing(bool value) async { + setState(() => isSharing = value); + + if (value) { + // Create a new share doc + final docRef = await firestore.collection('live_shares').add({ + 'route_id': widget.route.routeId, + 'device_id': widget.jeep.device_id, + 'sender': widget.user!.account_email, + 'is_sharing': true, + 'timestamp': FieldValue.serverTimestamp(), + }); + + final shareUrl = '${Uri.base.origin}/#/share?share_id=${docRef.id}'; + currentShareDocId = docRef.id; + + await Clipboard.setData(ClipboardData(text: shareUrl)); + + if (context.mounted) { + message('Copied Live Location Link'); + } + } else { + // Stop sharing by updating Firestore + if (currentShareDocId != null) { + await firestore + .collection('live_shares') + .doc(currentShareDocId) + .update({'is_sharing': false}); + } + + currentShareDocId = null; + message('Stopped Sharing Live Location'); + } + } + + Future loadSharingStatus() async { + final shareDoc = await firestore + .collection('live_shares') + .where('route_id', isEqualTo: widget.route.routeId) + .where('device_id', isEqualTo: widget.jeep.device_id) + .where('sender', + isEqualTo: widget.user!.account_email) // Check for current user + .orderBy('timestamp', + descending: true) // Order by timestamp in descending order + .limit(1) // Get the most recent sharing + .get(); + + if (shareDoc.docs.isNotEmpty) { + final doc = shareDoc.docs.first; + + setState(() { + isSharing = doc['is_sharing'] ?? false; + currentShareDocId = doc.id; + }); + } else { + resetSharingStatus(); + } + } + + void resetSharingStatus() { + setState(() { + isSharing = false; + currentShareDocId = null; + }); + } + + void message(String message) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + backgroundColor: Constants.bgColor, + title: Center( + child: Text( + message, + style: const TextStyle(color: Colors.white), + ))); + }); + } + @override Widget build(BuildContext context) { return Column( children: [ - if (widget.jeep.passenger_count == -2) - const SelectedJeepInfoRow( - left: Text("Driver disabled passenger counting.", - style: TextStyle(fontSize: 12)), - right: SizedBox()), - if (widget.jeep.passenger_count != -2) - SelectedJeepInfoRow( - left: Row( - children: [ - Icon(Icons.supervisor_account, - color: Color(widget.route.routeColor), size: 15), - const SizedBox(width: Constants.defaultPadding / 2), - const Text("Occupancy"), - ], - ), - right: Text(widget.jeep.passenger_count == -1 - ? "Available" - : widget.jeep.passenger_count == widget.jeep.max_capacity - ? "Full" - : "${widget.jeep.passenger_count}/${widget.jeep.max_capacity}")), + SelectedJeepInfoRow( + left: Row( + children: [ + Icon(Icons.supervisor_account, + color: Color(widget.route.routeColor), size: 15), + const SizedBox(width: Constants.defaultPadding / 2), + const Text("Occupancy"), + ], + ), + right: Text(widget.jeep.passenger_count < 0 + ? "Available" + : widget.jeep.passenger_count >= widget.jeep.max_capacity + ? "Full" + : "${widget.jeep.passenger_count}/${widget.jeep.max_capacity}")), const Divider(color: Colors.white), SelectedJeepInfoRow( left: Row(children: [ @@ -334,11 +432,30 @@ class _SelectedJeepInfoBoxState extends State { maxLines: 1, overflow: TextOverflow.ellipsis)), if (widget.user != null && widget.user!.is_verified && - widget.driver != null) + widget.driver != null) ...[ const Divider(color: Colors.white), - if (widget.user != null && - widget.user!.is_verified && - widget.driver != null) + SelectedJeepInfoRow( + left: Text("Share Live Location"), + right: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + onPressed: () => _toggleSharing(!isSharing), + icon: Icon( + isSharing ? Icons.stop_circle_rounded : Icons.share, + color: isSharing + ? Colors.red[600] + : Color(widget.route.routeColor), + size: 16, + ), + tooltip: isSharing ? 'Stop Sharing' : 'Share Location', + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + ), + ], + ), + ), + const SizedBox(height: Constants.defaultPadding / 2), Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ IconButtonBig( color: Color(widget.route.routeColor), @@ -392,6 +509,7 @@ class _SelectedJeepInfoBoxState extends State { ], )) ]) + ], ], ); } diff --git a/transitrack_web/lib/components/share_live_loc/share_desktop_route_info.dart b/transitrack_web/lib/components/share_live_loc/share_desktop_route_info.dart new file mode 100644 index 0000000..1ed4896 --- /dev/null +++ b/transitrack_web/lib/components/share_live_loc/share_desktop_route_info.dart @@ -0,0 +1,354 @@ +// ignore_for_file: non_constant_identifier_names + +// import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:mapbox_gl/mapbox_gl.dart'; +import 'dart:async'; + +import '../../models/account_model.dart'; +import '../../models/jeep_model.dart'; +import '../../models/route_model.dart'; +import '../../style/constants.dart'; +import './share_selected_jeep_info.dart'; +// import '../../services/eta.dart'; +// import '../select_jeep_prompt.dart'; +// import '../../components/cooldown_button.dart'; +// import '../../services/send_ping.dart'; + +// import '../../components/fare_matrix.dart'; +// This widget displays all the information for the route for the desktop view + +class ShareDesktopRouteInfo extends StatefulWidget { + // final bool gpsPermission; + final RouteData route; + final List jeeps; + final JeepsAndDrivers? selectedJeep; + final AccountData? user; + // final ValueChanged sendPing; + // final ValueChanged> etaCoordinates; + // final LatLng? myLocation; + const ShareDesktopRouteInfo({ + super.key, + // required this.gpsPermission, + required this.route, + required this.user, + required this.jeeps, + required this.selectedJeep, + // required this.etaCoordinates, + // required this.sendPing, + // required this.myLocation + }); + + @override + State createState() => _ShareDesktopRouteInfoState(); +} + +class _ShareDesktopRouteInfoState extends State { + late RouteData _value; + late List _jeeps; + late JeepsAndDrivers? _selectedJeep; + // late String? _eta; + late int operating; + late int not_operating; + // late LatLng? _myLocation; + // late bool _gpsPermission; + + late Timer etaFetcher; + + // AccountData? driverInfo; + bool isTapped = false; + + @override + void initState() { + super.initState(); + + setState(() { + // _gpsPermission = widget.gpsPermission; + _value = widget.route; + _jeeps = widget.jeeps; + _selectedJeep = widget.selectedJeep; + // _myLocation = widget.myLocation; + operating = widget.jeeps.where((jeep) => jeep.driver != null).length; + // _eta = null; + not_operating = widget.jeeps.where((jeep) => jeep.driver == null).length; + }); + + // etaFetcher = Timer.periodic(const Duration(seconds: 3), fetchEta); + } + + // void errorMessage(String message) { + // showDialog( + // context: context, + // builder: (context) { + // return AlertDialog( + // backgroundColor: Constants.bgColor, + // title: Center( + // child: Text( + // message, + // style: const TextStyle(color: Colors.white), + // ))); + // }); + // } + + // void fetchEta(Timer timer) async { + // if (_myLocation != null && _selectedJeep != null) { + // EtaData? result = await eta( + // widget.route.routeCoordinates, + // widget.route.isClockwise, + // _myLocation!, + // LatLng(_selectedJeep!.jeep.location.latitude, + // _selectedJeep!.jeep.location.longitude)); + // if (result != null) { + // setState(() { + // _eta = result.etaTime; + // }); + + // widget.etaCoordinates(result.etaCoordinates); + // } + // } + // } + + @override + void didUpdateWidget(covariant ShareDesktopRouteInfo oldWidget) { + super.didUpdateWidget(oldWidget); + + // if (widget.gpsPermission != _gpsPermission) { + // setState(() { + // _gpsPermission = widget.gpsPermission; + // }); + // } + + // if route choice changed + // if (widget.route != _value) { + // setState(() { + // _value = widget.route; + // }); + // } + + // if (widget.myLocation != _myLocation) { + // setState(() { + // _myLocation = widget.myLocation; + // }); + // } + + if (widget.selectedJeep != _selectedJeep) { + if (_selectedJeep != null && + widget.selectedJeep != null && + _selectedJeep!.jeep.device_id != + widget.selectedJeep!.jeep.device_id) { + setState(() { + // driverInfo = null; + // _eta = null; + }); + } + + setState(() { + _selectedJeep = widget.selectedJeep; + }); + } + + if (widget.jeeps != _jeeps) { + setState(() { + _jeeps = widget.jeeps; + operating = widget.jeeps.where((jeep) => jeep.driver != null).length; + not_operating = + widget.jeeps.where((jeep) => jeep.driver == null).length; + }); + } + } + + @override + void dispose() { + // etaFetcher.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + width: 300, + margin: const EdgeInsets.all(Constants.defaultPadding), + padding: const EdgeInsets.all(Constants.defaultPadding), + decoration: const BoxDecoration( + color: Constants.secondaryColor, + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + // Expanded( + // child: Padding( + // padding: const EdgeInsets.symmetric(vertical: 5.5), + // child: Text( + // _value.routeName, + // style: + // const TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // ), + // )), + // FareMatrix(route: widget.route), + // const SizedBox(width: Constants.defaultPadding / 2), + // if (widget.user != null) + // Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + // const Text("Wait a Ride", + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // style: TextStyle(fontSize: 10)), + // const SizedBox(width: Constants.defaultPadding / 3), + // if (_gpsPermission) + // CooldownButton( + // onPressed: () async { + // int result = await sendPing(widget.user!.account_email, + // _myLocation!, _value.routeId); + // if (result == 0) { + // widget.sendPing(true); + // } else { + // errorMessage("Failed to send your current location"); + // } + // }, + // alert: "Broadcasting your location...", + // verified: widget.user!.is_verified && _myLocation != null, + // child: _myLocation != null + // ? const Icon(Icons.location_on, size: 15) + // : const SizedBox( + // width: 15, + // height: 15, + // child: CircularProgressIndicator( + // color: Constants.bgColor, + // ))), + // if (!_gpsPermission) + // Container( + // width: 25, + // height: 25, + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(50), + // ), + // child: Center( + // child: Icon( + // Icons.location_off, + // size: 15, + // color: Colors.red[600], + // )), + // ), + // ]) + // ]), + // const SizedBox(height: Constants.defaultPadding), + Column(children: [ + // const SizedBox(height: Constants.defaultPadding * 7), + // const Divider(), + // const SizedBox(height: Constants.defaultPadding), + // if (_selectedJeep == null) const SelectJeepPrompt(), + if (_selectedJeep != null) + ShareSelectedJeepInfo( + // gpsPermission: widget.gpsPermission, + jeep: _selectedJeep!, + // eta: _eta, + user: widget.user, + route: _value), + ]), + // Stack(children: [ + // ClipRect( + // clipper: TopClipper(), + // child: SizedBox( + // height: 200, + // child: Stack( + // children: [ + // PieChart( + // PieChartData( + // sectionsSpace: 0, + // centerSpaceRadius: 70, + // startDegreeOffset: -180, + // sections: [ + // PieChartSectionData( + // color: Color(widget.route.routeColor), + // value: operating.toDouble(), + // showTitle: false, + // radius: 20, + // ), + // PieChartSectionData( + // color: Color(widget.route.routeColor) + // .withValues(alpha: 0.1), + // value: not_operating.toDouble(), + // showTitle: false, + // radius: 20, + // ), + // PieChartSectionData( + // color: Colors.transparent, + // value: (not_operating + operating).toDouble(), + // showTitle: false, + // radius: 20, + // ), + // ], + // ), + // ), + // Center( + // child: Column( + // mainAxisAlignment: MainAxisAlignment.start, + // children: [ + // const SizedBox(height: Constants.defaultPadding * 3), + // RichText( + // textAlign: TextAlign.center, + // text: TextSpan( + // children: [ + // TextSpan( + // text: '$operating', + // style: Theme.of(context) + // .textTheme + // .displaySmall + // ?.copyWith( + // color: Colors.white, + // fontWeight: FontWeight.w600, + // fontSize: 20, + // ), + // ), + // TextSpan( + // text: "/${operating + not_operating}", + // style: Theme.of(context) + // .textTheme + // .displaySmall + // ?.copyWith( + // color: Colors.white, + // fontWeight: FontWeight.w800, + // fontSize: 16, + // ), + // ), + // TextSpan( + // text: '\noperating', + // style: Theme.of(context) + // .textTheme + // .displaySmall + // ?.copyWith( + // color: Colors.white, + // fontWeight: FontWeight.w600, + // fontSize: 17, + // ), + // ), + // ], + // ), + // ), + // ], + // )), + // ], + // ), + // )), + // ]) + ], + ), + ); + } +} + +class TopClipper extends CustomClipper { + @override + Rect getClip(Size size) { + return Rect.fromLTWH(0, 0, size.width, 110); // Clip to top 120 pixels + } + + @override + bool shouldReclip(covariant CustomClipper oldClipper) { + return false; + } +} diff --git a/transitrack_web/lib/components/share_live_loc/share_map.dart b/transitrack_web/lib/components/share_live_loc/share_map.dart new file mode 100644 index 0000000..efe7c86 --- /dev/null +++ b/transitrack_web/lib/components/share_live_loc/share_map.dart @@ -0,0 +1,1069 @@ +// ignore_for_file: prefer_null_aware_operators, non_constant_identifier_names + +import 'dart:async'; +import 'dart:ui'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:mapbox_gl/mapbox_gl.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:transitrack_web/services/int_to_hex.dart'; +// import 'package:transitrack_web/services/mapbox/add_eta_line.dart'; +import 'package:transitrack_web/services/mapbox/add_image_from_asset.dart'; +// import 'package:transitrack_web/services/mapbox/animate_ripple.dart'; +// import 'package:transitrack_web/services/mapbox/request_location.dart'; +// import 'package:transitrack_web/services/mapbox/minute_old_checker.dart'; + +import '../../config/keys.dart'; +import '../../config/map_settings.dart'; +import '../../config/responsive.dart'; +import '../../models/account_model.dart'; +import '../../models/jeep_model.dart'; +import '../../models/route_model.dart'; +// import '../../models/ping_model.dart'; + +import '../../style/constants.dart'; +// import '../account_related/route_manager/route_manager_options.dart'; +import './share_desktop_route_info.dart'; +// import '../right_panel/unselected_desktop_route_info.dart'; +import '../right_panel/mobile_dashboard_unselected.dart'; +import './share_mobile_route_info.dart'; + +// This is the main map + +class ShareMapWidget extends StatefulWidget { + final RouteData? route; + final List? jeeps; + final AccountData? currentUserFirestore; + final ValueChanged foundDeviceLocation; + final ValueChanged mapLoaded; + const ShareMapWidget( + {super.key, + required this.route, + required this.jeeps, + required this.currentUserFirestore, + required this.foundDeviceLocation, + required this.mapLoaded}); + + @override + State createState() => _ShareMapWidgetState(); +} + +class _ShareMapWidgetState extends State + with TickerProviderStateMixin { + late RouteData? _value; + late List? jeeps; + late LatLng? myLocation; + late int _configRoute; + // late int _configStops; + + late MapboxMapController _mapController; + late StreamSubscription _positionStream; + + // late Circle? + // deviceCircle; // the white circle showing the passengers current location + bool mapLoaded = false; // used to trigger actions once map is fully loaded + + // for route points, lines, and jeepneys + late List setRoute; + // late List setRouteCopy; + // late List setStops; + // LatLng? selectedStop; // To store the selected stop's coordinates + // List circles = []; + // List stopsCircles = []; + List lines = []; + List jeepEntities = []; + + // Ping Fetching + // StreamSubscription? pingListener; + // List pings = []; + // late Timer timer; + + // set to true once user allows location service + bool gpsTracking = false; + + JeepEntity? selectedJeep; + + @override + void initState() { + super.initState(); + + setState(() { + _value = widget.route; + jeeps = widget.jeeps; + _configRoute = -1; + // _configStops = -1; + myLocation = null; + // deviceCircle = null; + }); + + // if the map is loaded refresh the available pings + // if (mapLoaded) { + // refreshPingLayer(); + // } + + // runs every second to update the pings displayed on the map + // timer = Timer.periodic(const Duration(seconds: 1), (Timer timer) { + // // if map is fully loaded and routeData is not null + // if (mapLoaded && _value != null) { + // // we set pings to those ping's within the 1 minute duration + // setState(() { + // pings = pings + // .where((element) => + // minuteOldChecker(element.ping_timestamp.toDate())) + // .toList(); + // }); + + // // converts pings as a GeoJSON format to set as the source for the pings layer + // _mapController.setGeoJsonSource("pings", listToGeoJSON(pings)); + // } + // }); + } + + // triggers request loc permission and if allowed listen to location + // void startListening() async { + // var permission = await requestLocationPermission(context); + + // setState(() { + // gpsTracking = permission; + // }); + + // if (gpsTracking) { + // _listenToDeviceLocation(); + // } + // } + + @override + // if the user selects a new route update the map information + void didUpdateWidget(covariant ShareMapWidget oldWidget) { + super.didUpdateWidget(oldWidget); + // if route choice changed + if (widget.route != _value) { + selectedJeep = null; + // _mapController.setGeoJsonSource("eta", etaListToGeoJSON([])); + + // if (_value == null) { + // _mapController.onSymbolTapped.add(onJeepTapped); + // } else { + // if (widget.route == null) { + // _mapController.onSymbolTapped.remove(onJeepTapped); + // } + // } + + setState(() { + _value = widget.route; + }); + + // route lines and pings update + addLine(); + // refreshPingLayer(); + + // clear jeepney entities in the map + _mapController.clearSymbols().then((value) => jeepEntities.clear()); + } + + // jeepney updates + if (widget.jeeps != jeeps) { + setState(() { + jeeps = widget.jeeps; + }); + + updateJeeps(); + } + } + + // update jeepney entities + void updateJeeps() { + if (!mapLoaded) { + return; + } + + List? toUpdate = jeeps; + if (toUpdate != null && toUpdate.isNotEmpty) { + JeepsAndDrivers jeep = toUpdate.first; + + // Remove jeeps not matching current one + for (var device_id in jeepEntities + .map((e) => e.jeepAndDriver.jeep.device_id) + .toSet() + .difference({jeep.jeep.device_id})) { + int index = jeepEntities.indexWhere( + (element) => element.jeepAndDriver.jeep.device_id == device_id); + _mapController.removeSymbol(jeepEntities[index].jeepSymbol); + jeepEntities.removeAt(index); + } + + // Update or add the only jeep + if (jeepEntities + .any((e) => e.jeepAndDriver.jeep.device_id == jeep.jeep.device_id)) { + int index = jeepEntities.indexWhere( + (e) => e.jeepAndDriver.jeep.device_id == jeep.jeep.device_id); + _mapController.updateSymbol( + jeepEntities[index].jeepSymbol, + SymbolOptions( + geometry: LatLng( + jeep.jeep.location.latitude, jeep.jeep.location.longitude), + iconRotate: jeep.jeep.bearing, + iconSize: jeep.driver != null ? 1 : 0, + textSize: jeep.driver != null ? 50 : 0, + textRotate: jeep.jeep.bearing + 90)); + jeepEntities[index].setJeepsAndDrivers(jeep); + setState(() { + selectedJeep = jeepEntities[index]; + }); + } else { + _mapController + .addSymbol(SymbolOptions( + geometry: + LatLng(jeep.jeep.location.latitude, jeep.jeep.location.longitude), + iconImage: "jeepTopSelected", // Always use selected icon + textField: "▬▬", + textLetterSpacing: -0.35, + textColor: intToHexColor(widget.route!.routeColor), + textRotate: jeep.jeep.bearing + 90, + iconRotate: jeep.jeep.bearing, + iconSize: jeep.driver != null ? 1 : 0, + textSize: jeep.driver != null ? 50 : 0, + )) + .then((symbol) { + final newEntity = JeepEntity(jeepAndDriver: jeep, jeepSymbol: symbol); + setState(() { + jeepEntities.add(newEntity); + selectedJeep = newEntity; + }); + }); + } + } else { + selectedJeep = null; + _mapController.clearSymbols(); + jeepEntities.clear(); + } + } + + // animate the users location indicator circle + // void animateCircleMovement(LatLng from, LatLng to, Circle circle, + // TickerProvider tick, MapboxMapController mapController) { + // final animationController = AnimationController( + // vsync: tick, + // duration: const Duration(milliseconds: 500), + // ); + // final animation = LatLngTween(begin: from, end: to).animate(CurvedAnimation( + // parent: animationController, + // curve: Curves.easeInOut, + // )); + + // animation.addListener(() { + // mapController.updateCircle( + // circle, CircleOptions(geometry: animation.value)); + // }); + + // animation.addStatusListener((status) { + // if (status == AnimationStatus.completed) { + // animationController.dispose(); + // } + // }); + + // animationController.forward(); + // } + + // sets the map controller when the Mapbox map is created + // controller - instance of a class that allows you to perform operations + void _onMapCreated(MapboxMapController controller) { + _mapController = controller; + } + + // check and update user location circle within the map + // void _listenToDeviceLocation() { + // _positionStream = Geolocator.getPositionStream( + // locationSettings: const LocationSettings( + // accuracy: LocationAccuracy.best, + // ), + // ).listen((Position position) { + // _updateDeviceCircle(LatLng(position.latitude, position.longitude)); + // }); + // } + + // checks if there are new pings available + // void listenToPingsFirestore() { + // pingListener = FirebaseFirestore.instance + // .collection('pings') + // .where('ping_route', isEqualTo: _value!.routeId) + // .snapshots() + // .listen((QuerySnapshot snapshot) { + // if (snapshot.docs.isNotEmpty) { + // setState(() { + // pings = snapshot.docs + // .map((doc) => PingData.fromFirestore(doc)) + // .where((element) => + // minuteOldChecker(element.ping_timestamp.toDate())) + // .toList(); + // }); + // _mapController.setGeoJsonSource("pings", listToGeoJSON(pings)); + // } + // }); + // } + + // update pings within the map + // void refreshPingLayer() { + // if (_value != null) { + // addGeojsonCluster(_mapController, _value!); + // listenToPingsFirestore(); + // } else { + // pingListener?.cancel(); + // pings.clear(); + // // updates the pings layer of the map to display the current pings listed in GeoJSON format + // _mapController.setGeoJsonSource("pings", listToGeoJSON(pings)); + // } + // } + + // update the user's location indicator + // void _updateDeviceCircle(LatLng latLng) { + // setState(() { + // myLocation = latLng; + // }); + // if (deviceCircle != null) { + // animateCircleMovement(deviceCircle!.options.geometry as LatLng, latLng, + // deviceCircle!, this, _mapController); + // } else { + // _mapController + // .addCircle(CircleOptions( + // geometry: latLng, + // circleRadius: 5, + // circleColor: deviceCircleColor, + // circleStrokeWidth: 2, + // circleStrokeColor: '#FFFFFF')) + // .then((circle) { + // deviceCircle = circle; + // }); + // // _mapController + // // .animateCamera(CameraUpdate.newLatLngZoom(myLocation!, mapStartZoom)); + // widget.foundDeviceLocation(latLng); + // } + // } + + void errorMessage(String message) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + backgroundColor: Constants.bgColor, + title: Center( + child: Text( + message, + style: const TextStyle(color: Colors.white), + ))); + }); + } + + // update route + void update(String collectionName, List coordinates) async { + // show loading circle + showDialog( + context: context, + builder: (context) { + return const Center(child: CircularProgressIndicator()); + }); + + // updates route coordinates + try { + Map newAccountSettings = { + collectionName: coordinates + .map((latLng) => GeoPoint(latLng.latitude, latLng.longitude)) + .toList() + }; + + RouteData.updateRouteFirestore(widget.route!.routeId, newAccountSettings) + .then((value) => Navigator.pop(context)) + .then((value) => Navigator.pop(context)); + + errorMessage("Success!"); + } catch (e) { + // pop loading circle + Navigator.pop(context); + errorMessage(e.toString()); + } + } + + // initializes the lines of the map for the route + void addLine() { + _mapController.clearLines().then((value) => lines.clear()); + if (widget.route != null) { + for (int i = 0; + i < + (_configRoute == -1 + ? widget.route!.routeCoordinates.length + : setRoute.length); + i++) { + _mapController + .addLine(LineOptions( + lineWidth: 4.0, + lineColor: intToHexColor(widget.route!.routeColor), + lineOpacity: 0.5, + geometry: i != + (_configRoute == -1 + ? widget.route!.routeCoordinates.length + : setRoute.length) - + 1 + ? (_configRoute == -1 + ? [ + widget.route!.routeCoordinates[i], + widget.route!.routeCoordinates[i + 1] + ] + : [setRoute[i], setRoute[i + 1]]) + : (_configRoute == -1 + ? [ + widget.route!.routeCoordinates[i], + widget.route!.routeCoordinates[0] + ] + : [setRoute[i], setRoute[0]]), + )) + .then((line) => lines.add(line)); + } + } + } + + // initializes the points on the map + // void addPoints(List circlePoints, List coords, int config) { + // // make circles and coord array parameters + // // remove the coordinate circles within the map + // for (var circle in circlePoints) { + // _mapController.removeCircle(circle); + // } + + // // clear the array containing the coordinate circles + // circlePoints.clear(); + + // // for each route coordinate, add a circle coordinate and add it in the circle coordinates array + // for (int i = 0; i < coords.length; i++) { + // _mapController + // .addCircle(CircleOptions( + // circleRadius: 8.0, + // circleStrokeWidth: 2.0, + // circleStrokeOpacity: 1, + // circleColor: intToHexColor(widget.route!.routeColor), + // geometry: coords[i], + // circleStrokeColor: '#FFFFFF', + // draggable: config == 0 ? true : false)) + // .then((circle) => circlePoints.add(circle)); + // } + // } + + // when line on the map is tapped it adds another coordinate + // it clears the coordinates and reinitialize them again along with the lines + // void onLineTapped(Line pressedLine) { + // // add bool to reuse this funct and use circles and coord array a parameter to reuse + // int index = lines.indexWhere((line) => pressedLine == line); + + // double x = (pressedLine.options.geometry![0].latitude + + // pressedLine.options.geometry![1].latitude) / + // 2; + // double y = (pressedLine.options.geometry![0].longitude + + // pressedLine.options.geometry![1].longitude) / + // 2; + + // setRoute.insert(index + 1, LatLng(x, y)); + + // for (var circle in circles) { + // _mapController.removeCircle(circle); + // } + // circles.clear(); + // addPoints(circles, setRoute, _configRoute); + // _mapController + // .clearLines() + // .then((value) => lines.clear()) + // .then((value) => addLine()); + // } + + // if a coordinate is tapped it would be removed + // it will call add points to reinitialize all points again + // it will call add line to reinitilize all lines again + // void onCircleTapped(Circle pressedCircle) { + // // make circles and coord array a paramter + // int index = circles.indexWhere((circle) => pressedCircle == circle); + + // if (index != -1) { + // setRoute.removeAt(index); + // addPoints(circles, setRoute, _configRoute); + // addLine(); + // } + // } + + // add stop points within the map + // void onStopLocationTapped(LatLng tappedLocation) { + // // Add the tapped location to the stops list + // setStops.add(tappedLocation); + + // // Update the selected stop + // setState(() { + // selectedStop = tappedLocation; + // }); + // // Remove existing stop circles from the map + // for (var circle in stopsCircles) { + // _mapController.removeCircle(circle); + // } + // stopsCircles.clear(); + + // // Reinitialize the stop circles on the map + // addPoints(stopsCircles, setStops, _configStops); + // } + + // void onStopLocationCircleTapped(Circle pressedCircle) { + // // Find the index of the tapped stop circle + // int index = stopsCircles.indexWhere((circle) => pressedCircle == circle); + + // // If the circle exists in the stopsCircles list + // if (index != -1) { + // // Update the selected stop + // setState(() { + // selectedStop = stopsCircles[index].options.geometry; + // }); + // // Remove the stop location from the setStops list + // setStops.removeAt(index); + + // // Reinitialize the stop circles on the map + // addPoints(stopsCircles, setStops, _configStops); + // } + // } + + // void onStopLocationCircleSelected(Circle pressedCircle) { + // // Find the index of the tapped stop circle + // int index = stopsCircles.indexWhere((circle) => pressedCircle == circle); + + // // If the circle exists in the stopsCircles list + // if (index != -1) { + // // Update the selected stop + // setState(() { + // selectedStop = stopsCircles[index].options.geometry; + // }); + // } + // } + + // jeep selection on the map + // void onJeepTapped(Symbol pressedJeep) { + // if (selectedJeep != null) { + // if (pressedJeep != selectedJeep!.jeepSymbol) { + // if (jeepEntities + // .any((jeepEntity) => jeepEntity.jeepSymbol == pressedJeep)) { + // setState(() { + // selectedJeep = jeepEntities.firstWhere( + // (jeepEntity) => jeepEntity.jeepSymbol == pressedJeep); + // }); + // } + // } else { + // setState(() { + // selectedJeep = null; + // }); + + // _mapController.setGeoJsonSource("eta", etaListToGeoJSON([])); + // } + // } else { + // if (jeepEntities + // .any((jeepEntity) => jeepEntity.jeepSymbol == pressedJeep)) { + // setState(() { + // selectedJeep = jeepEntities + // .firstWhere((jeepEntity) => jeepEntity.jeepSymbol == pressedJeep); + // }); + // } + // } + + // for (var jeep in jeepEntities) { + // _mapController.updateSymbol( + // jeep.jeepSymbol, const SymbolOptions(iconImage: 'jeepTop')); + // } + + // if (selectedJeep != null) { + // _mapController.updateSymbol(selectedJeep!.jeepSymbol, + // const SymbolOptions(iconImage: 'jeepTopSelected')); + // } + // } + + @override + void dispose() { + _mapController.dispose(); + _positionStream.cancel(); + super.dispose(); + } + + void resetCamera() { + _mapController.animateCamera( + CameraUpdate.newLatLngZoom(Keys.MapCenter, mapStartZoom)); + } + + // void resetToLocation() { + // _mapController + // .animateCamera(CameraUpdate.newLatLngZoom(myLocation!, mapStartZoom)); + // } + + void _onStyleLoadedCallback() { + setState(() { + mapLoaded = true; + }); + widget.mapLoaded(true); + + // Ensure these operations happen in sequence + // addETALayer(_mapController); + addImageFromAsset(_mapController).then((_) { + _mapController.setSymbolIconAllowOverlap(true); + _mapController.setSymbolTextAllowOverlap(true); + _mapController.setSymbolIconIgnorePlacement(true); + _mapController.setSymbolTextIgnorePlacement(true); + + // Only try to add lines and jeeps after everything is ready + if (widget.route != null) { + addLine(); + } + if (widget.jeeps != null && widget.jeeps!.isNotEmpty) { + updateJeeps(); + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + Column(children: [ + Expanded( + child: MapboxMap( + accessToken: Keys.MapBoxKey, + styleString: Keys.MapBoxNight, + doubleClickZoomEnabled: false, + minMaxZoomPreference: + MinMaxZoomPreference(mapMinZoom, mapMaxZoom), + compassEnabled: true, + compassViewPosition: Responsive.isDesktop(context) + ? CompassViewPosition.TopLeft + : CompassViewPosition.TopRight, + onMapCreated: (controller) { + _onMapCreated(controller); + }, + // allows you to perform additional setup and customization of the map once the style has been fully loaded + onStyleLoadedCallback: _onStyleLoadedCallback, + initialCameraPosition: CameraPosition( + target: Keys.MapCenter, + zoom: mapStartZoom, + ), + onMapClick: (point, latLng) { + // if (_configRoute == 1) { + // setRoute.add(latLng); + + // addPoints(circles, setRoute, _configRoute); + // addLine(); + // } + // if (_configStops == 1) { + // // Add a new stop location + // onStopLocationTapped(latLng); + // } + }, + ), + ), + if (Responsive.isMobile(context)) + Container( + height: 220, + decoration: const BoxDecoration( + color: Constants.secondaryColor, + ), + child: widget.route == null + ? const MobileDashboardUnselected() + : ShareMobileRouteInfo( + // gpsPermission: gpsTracking, + route: _value!, + jeeps: jeeps!, + selectedJeep: selectedJeep != null + ? selectedJeep!.jeepAndDriver + : null, + user: widget.currentUserFirestore, + // sendPing: (bool value) async { + // _mapController.animateCamera( + // CameraUpdate.newLatLngZoom( + // myLocation!, mapStartZoom)); + + // LatLng pingLoc = myLocation!; + // for (int i = 0; i < 3; i++) { + // animateRipple( + // _mapController, _value, this, pingLoc); + + // await Future.delayed( + // const Duration(milliseconds: 2000)); + // } + // }, + // etaCoordinates: (List etaCoordinates) async { + // if (selectedJeep != null) { + // await _mapController.setGeoJsonSource( + // "eta", etaListToGeoJSON(etaCoordinates)); + // } + // }, + // myLocation: myLocation + )) + ]), + Positioned( + right: Responsive.isDesktop(context) + ? null + : Constants.defaultPadding * 3, + left: Responsive.isDesktop(context) + ? Constants.defaultPadding * 3 + : null, + top: Constants.defaultPadding * 0.75, + child: Row( + children: [ + PointerInterceptor( + child: CircleAvatar( + radius: 13, + backgroundColor: Colors.grey[100], + child: IconButton( + onPressed: () => resetCamera(), + icon: const Icon(Icons.center_focus_strong), + tooltip: "Reset Map Location", + visualDensity: VisualDensity.compact, + iconSize: 10, + color: Colors.black, + ), + ), + ), + // if (myLocation != null) + // const SizedBox(width: Constants.defaultPadding / 2), + // if (myLocation != null) + // PointerInterceptor( + // child: CircleAvatar( + // radius: 13, + // backgroundColor: Colors.grey[100], + // child: IconButton( + // onPressed: () => resetToLocation(), + // icon: const Icon(Icons.person), + // tooltip: "Reset to your Location", + // visualDensity: VisualDensity.compact, + // iconSize: 10, + // color: Colors.black, + // ), + // ), + // ), + ], + )), + if (!Responsive.isMobile(context)) + Positioned( + top: 0, + right: 0, + child: Column( + children: [ + // if (widget.route == null) + // PointerInterceptor( + // child: const UnselectedDesktopRouteInfo()), + + if (widget.route != null) + PointerInterceptor( + child: ShareDesktopRouteInfo( + // gpsPermission: gpsTracking, + route: _value!, + jeeps: jeeps!, + selectedJeep: selectedJeep != null + ? selectedJeep!.jeepAndDriver + : null, + user: widget.currentUserFirestore, + // sendPing: (bool value) async { + // _mapController.animateCamera( + // CameraUpdate.newLatLngZoom( + // myLocation!, mapStartZoom)); + + // LatLng pingLoc = myLocation!; + // for (int i = 0; i < 3; i++) { + // animateRipple( + // _mapController, _value, this, pingLoc); + + // await Future.delayed( + // const Duration(milliseconds: 2000)); + // } + // }, + // etaCoordinates: (List etaCoordinates) async { + // if (selectedJeep != null) { + // await _mapController.setGeoJsonSource( + // "eta", etaListToGeoJSON(etaCoordinates)); + // } + // }, + // myLocation: myLocation, + ), + ), + + // Route Manager Dashboard + // if (widget.route != null && + // widget.currentUserFirestore != null && + // widget.currentUserFirestore!.account_type == 2 && + // widget.currentUserFirestore!.is_verified && + // widget.route!.routeId == + // widget.currentUserFirestore!.route_id) + // Container( + // width: 300, + // padding: + // const EdgeInsets.all(Constants.defaultPadding), + // margin: const EdgeInsets.symmetric( + // horizontal: Constants.defaultPadding), + // decoration: const BoxDecoration( + // color: Constants.secondaryColor, + // borderRadius: BorderRadius.all(Radius.circular(10)), + // ), + // child: RouteManagerOptions( + // route: widget.route!, + // jeeps: widget.jeeps!, + // pressedJeep: (JeepsAndDrivers searchedJeep) { + // selectedJeep = null; + // onJeepTapped(jeepEntities + // .firstWhere((element) => + // element.jeepAndDriver.jeep.device_id == + // searchedJeep.jeep.device_id) + // .jeepSymbol); + // _mapController.animateCamera( + // CameraUpdate.newLatLngZoom( + // LatLng( + // searchedJeep.jeep.location.latitude, + // searchedJeep.jeep.location.longitude), + // mapStartZoom)); + // }, + // coordConfig: (int coordConfig) { + // // we save what is the previous mode + // int prev = _configRoute; + + // setState(() { + // _configRoute = coordConfig; + // }); + + // // unselected any of the choices + // if (_configRoute < 0) { + // print("prev mode: $prev"); + // print("configRoute mode: $_configRoute"); + // // Coming from moving points, we save the new coordinates. + // if (prev == 0) { + // if (_configRoute == -1) { + // // clear the array containing route coordinates + // setRoute.clear(); + + // // repopulate the array with the new coordinates + // for (var circle in circles) { + // setRoute.add(circle.options.geometry!); + // } + // // remove the coordinate circles in the map + // for (var circle in circles) { + // _mapController.removeCircle(circle); + // } + + // // clear the array containing coordinate circles + // circles.clear(); + // // re-initialize the route lines + // addLine(); + // // Update Firestore + // update('route_coordinates', setRoute); + // } else if (_configRoute == -2) { + // // remove the coordinate circles in the map + // for (var circle in circles) { + // _mapController.removeCircle(circle); + // } + + // // clear the array containing coordinate circles + // circles.clear(); + // // re-initialize the route lines + // addLine(); + // } + + // // if the mode is previously add/delete state + // } else if (prev == 1) { + // if (_configRoute == -1) { + // // remove onTap listeners to ensure that tapping on the map would no longer add/delete points + // _mapController.onCircleTapped + // .remove(onCircleTapped); + // _mapController.onLineTapped + // .remove(onLineTapped); + // // remove the coordinate circles in the map + // for (var circle in circles) { + // _mapController.removeCircle(circle); + // } + // // clear the array containing coordinate circles + // circles.clear(); + // update('route_coordinates', setRoute); + // } else if (_configRoute == -2) { + // _mapController.onCircleTapped + // .remove(onCircleTapped); + // _mapController.onLineTapped + // .remove(onLineTapped); + + // // Revert to the original route coordinates from the database + // setRoute.clear(); + // setRoute = List.from(widget + // .route!.routeCoordinates); // Deep copy + + // // Remove the coordinate circles in the map + // for (var circle in circles) { + // _mapController.removeCircle(circle); + // } + + // // Clear the array containing coordinate circles + // circles.clear(); + + // // Re-initialize the route lines + // addLine(); + // } + // } + + // // if not at default mode + // } else { + // // create a deep copy, otherwise the routeCoordinates would also be modified + // setRoute = List.from( + // widget.route!.routeCoordinates); + + // // Move points mode + // if (_configRoute == 0) { + // _mapController.clearLines(); + // // re-initialize the coordinates in the map + // addPoints(circles, setRoute, _configRoute); + // } + + // // Add or Remove Points + // else if (_configRoute == 1) { + // // add the on tap listeners + // _mapController.onLineTapped.add(onLineTapped); + // _mapController.onCircleTapped + // .add(onCircleTapped); + // // re-initialize the coordinates in the map + // addPoints(circles, setRoute, _configRoute); + // } + // } + // }, + // stopsConfig: (int stopsConfig) { + // // we save what is the previous mode + // int prev = _configStops; + + // // update _configStops to trigger state change + // // note that coordConfig is the variable modified in route_settings + // // and _configStops manages mode changes in the map + // setState(() { + // _configStops = stopsConfig; + // }); + + // // if at default mode + // if (_configStops < 0) { + // // Set selectedStop to null + // setState(() { + // selectedStop = null; + // }); + + // // if from moving points mode + // if (prev == 0) { + // // if changes are saved + // if (_configStops == -1) { + // // clear the array containing route coordinates + // setStops.clear(); + + // // repopulate the array with the new coordinates + // for (var circle in stopsCircles) { + // setStops.add(circle.options.geometry!); + // } + + // // Update the route's stopsCoordinates + // setState(() { + // widget.route!.stopsCoordinates = setStops; + // }); + + // // remove the coordinate circles in the map + // for (var circle in stopsCircles) { + // _mapController.removeCircle(circle); + // } + + // // clear the array containing coordinate circles + // stopsCircles.clear(); + // // re-initialize the route lines + // addLine(); + // // Update Firestore + // update('stops_coordinates', setStops); + // // if changes are not saved + // } else if (_configStops == -2) { + // // remove the coordinate circles in the map + // for (var circle in stopsCircles) { + // _mapController.removeCircle(circle); + // } + + // // clear the array containing coordinate circles + // stopsCircles.clear(); + // // re-initialize the route lines + // addLine(); + // } + + // // if from adding/deleting points mode + // } else if (prev == 1) { + // // if changes are saved + // if (_configStops == -1) { + // // remove onTap listeners to ensure that tapping on the map would no longer add/delete points + // _mapController.onCircleTapped + // .remove(onStopLocationCircleTapped); + // // remove the coordinate circles in the map + // for (var circle in stopsCircles) { + // _mapController.removeCircle(circle); + // } + // // clear the array containing coordinate circles + // stopsCircles.clear(); + // // Update the route's stopsCoordinates + // setState(() { + // widget.route!.stopsCoordinates = setStops; + // }); + // update('stops_coordinates', setStops); + // // if changes are not saved + // } else if (_configStops == -2) { + // _mapController.onCircleTapped + // .remove(onStopLocationCircleTapped); + + // // Revert to the original route coordinates from the database + // setStops.clear(); + // setStops = List.from(widget + // .route!.stopsCoordinates); // Deep copy + + // // Remove the coordinate circles in the map + // for (var circle in stopsCircles) { + // _mapController.removeCircle(circle); + // } + + // // Clear the array containing coordinate circles + // stopsCircles.clear(); + + // // Re-initialize the route lines + // addLine(); + // } + // } + // } else { + // // create a deep copy, otherwise the routeCoordinates would also be modified + // setStops = List.from( + // widget.route!.stopsCoordinates); + // // if at adding/deleting points mode + // if (_configStops == 0) { + // // Add the onCircleTapped listener for selecting stops + // _mapController.onCircleTapped + // .add(onStopLocationCircleSelected); + // addPoints( + // stopsCircles, setStops, _configStops); + + // // if at moving points mode + // } else if (_configStops == 1) { + // _mapController.onCircleTapped + // .add(onStopLocationCircleTapped); + // addPoints( + // stopsCircles, setStops, _configStops); + // } + // } + // }, + // selectedStop: selectedStop, + // )) + ], + )) + ], + ), + ); + } +} + +class LatLngTween extends Tween { + LatLngTween({required LatLng begin, required LatLng end}) + : super(begin: begin, end: end); + + @override + LatLng lerp(double t) => LatLng( + lerpDouble(begin!.latitude, end!.latitude, t)!, + lerpDouble(begin!.longitude, end!.longitude, t)!, + ); +} diff --git a/transitrack_web/lib/components/share_live_loc/share_mobile_route_info.dart b/transitrack_web/lib/components/share_live_loc/share_mobile_route_info.dart new file mode 100644 index 0000000..d6fe5f6 --- /dev/null +++ b/transitrack_web/lib/components/share_live_loc/share_mobile_route_info.dart @@ -0,0 +1,398 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:mapbox_gl/mapbox_gl.dart'; +// import 'dart:async'; + +import '../../models/account_model.dart'; +import '../../models/jeep_model.dart'; +import '../../models/route_model.dart'; +import '../../style/constants.dart'; +// import '../../services/eta.dart'; +import '../../services/format_time.dart'; +import '../select_jeep_prompt.dart'; +import './share_selected_jeep_info.dart'; +// import '../cooldown_button.dart'; +// import '../../services/send_ping.dart'; + +import '../../components/fare_matrix.dart'; + +// This widget displays all the information for the route for the mobile view + +class ShareMobileRouteInfo extends StatefulWidget { + // final bool gpsPermission; + final RouteData route; + final List jeeps; + final JeepsAndDrivers? selectedJeep; + final AccountData? user; + // final ValueChanged sendPing; + // final ValueChanged> etaCoordinates; + // final LatLng? myLocation; + const ShareMobileRouteInfo({ + super.key, + // required this.gpsPermission, + required this.route, + required this.jeeps, + required this.selectedJeep, + required this.user, + // required this.sendPing, + // required this.etaCoordinates, + // required this.myLocation + }); + + @override + State createState() => _ShareMobileRouteInfoState(); +} + +class _ShareMobileRouteInfoState extends State { + late RouteData _value; + late List _jeeps; + late JeepsAndDrivers? _selectedJeep; + // late String? _eta; + late int operating; + late int not_operating; + // late LatLng? _myLocation; + + // AccountData? driverInfo; + bool isTapped = false; + + // late Timer etaFetcher; + + @override + void initState() { + super.initState(); + + setState(() { + _value = widget.route; + _jeeps = widget.jeeps; + _selectedJeep = widget.selectedJeep; + // _myLocation = widget.myLocation; + operating = widget.jeeps.where((jeep) => jeep.driver != null).length; + // _eta = null; + not_operating = widget.jeeps.where((jeep) => jeep.driver == null).length; + }); + + // etaFetcher = Timer.periodic(const Duration(seconds: 3), fetchEta); + } + + void errorMessage(String message) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + backgroundColor: Constants.bgColor, + title: Center( + child: Text( + message, + style: const TextStyle(color: Colors.white), + ))); + }); + } + + // void fetchEta(Timer timer) async { + // if (_myLocation != null && _selectedJeep != null) { + // EtaData? result = await eta( + // widget.route.routeCoordinates, + // widget.route.isClockwise, + // _myLocation!, + // LatLng(_selectedJeep!.jeep.location.latitude, + // _selectedJeep!.jeep.location.longitude)); + // if (result != null) { + // setState(() { + // _eta = result.etaTime; + // }); + + // widget.etaCoordinates(result.etaCoordinates); + // } + // } + // } + + @override + void didUpdateWidget(covariant ShareMobileRouteInfo oldWidget) { + super.didUpdateWidget(oldWidget); + // if route choice changed + if (widget.route != _value) { + setState(() { + _value = widget.route; + }); + } + + // if (widget.myLocation != _myLocation) { + // setState(() { + // _myLocation = widget.myLocation; + // }); + // } + + if (widget.selectedJeep != _selectedJeep) { + if (_selectedJeep != null && + widget.selectedJeep != null && + _selectedJeep!.jeep.device_id != + widget.selectedJeep!.jeep.device_id) { + setState(() { + // driverInfo = null; + // _eta = null; + }); + } + + setState(() { + _selectedJeep = widget.selectedJeep; + }); + } + + if (widget.jeeps != _jeeps) { + setState(() { + _jeeps = widget.jeeps; + operating = widget.jeeps.where((jeep) => jeep.driver != null).length; + not_operating = + widget.jeeps.where((jeep) => jeep.driver == null).length; + }); + } + } + + @override + void dispose() { + // etaFetcher.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Column( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.all(Constants.defaultPadding), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // _value.routeName, + // style: const TextStyle( + // fontSize: 20, fontWeight: FontWeight.w500), + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // ), + // const SizedBox(width: Constants.defaultPadding), + // if (_selectedJeep != null) + // Text( + // "$operating/${operating + not_operating} operating", + // style: const TextStyle( + // fontSize: 14, fontWeight: FontWeight.w500), + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // ), + // ]), + if (_selectedJeep == null) + Stack(children: [ + Column(children: [ + const SizedBox(height: Constants.defaultPadding / 3), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ClipRect( + clipper: TopClipper(), + child: SizedBox( + height: 120, + child: Stack( + children: [ + PieChart( + PieChartData( + sectionsSpace: 0, + centerSpaceRadius: 50, + startDegreeOffset: -180, + sections: [ + PieChartSectionData( + color: Color(widget + .route.routeColor), + value: operating.toDouble(), + showTitle: false, + radius: 10, + ), + PieChartSectionData( + color: Color(widget + .route.routeColor) + .withValues(alpha: 0.1), + value: not_operating + .toDouble(), + showTitle: false, + radius: 10, + ), + PieChartSectionData( + color: Colors.transparent, + value: (not_operating + + operating) + .toDouble(), + showTitle: false, + radius: 10, + ), + ], + ), + ), + Center( + child: Column( + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + const SizedBox( + height: Constants + .defaultPadding), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: '$operating', + style: Theme.of(context) + .textTheme + .displaySmall + ?.copyWith( + color: + Colors.white, + fontWeight: + FontWeight + .w600, + fontSize: 18, + ), + ), + TextSpan( + text: + "/${operating + not_operating}", + style: Theme.of(context) + .textTheme + .displaySmall + ?.copyWith( + color: + Colors.white, + fontWeight: + FontWeight + .w800, + fontSize: 14, + ), + ), + TextSpan( + text: '\noperating', + style: Theme.of(context) + .textTheme + .displaySmall + ?.copyWith( + color: + Colors.white, + fontWeight: + FontWeight + .w600, + fontSize: 15, + ), + ), + ], + ), + ), + ], + )), + ], + ), + )), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // Text( + // "${_value.routeFare} Regular", + // style: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.w500), + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // ), + // Text( + // "${_value.routeFareDiscounted} Discounted", + // style: const TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.w500), + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // ), + FareMatrix(route: widget.route), + Text( + formatTime(_value.routeTime), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ]) + ]), + ]), + const Column(children: [ + SizedBox(height: Constants.defaultPadding * 4.5), + Divider(color: Colors.white), + SizedBox(height: Constants.defaultPadding / 2), + SelectJeepPrompt() + ]) + ]), + if (_selectedJeep != null) + Column(children: [ + const SizedBox(height: Constants.defaultPadding), + ShareSelectedJeepInfo( + // gpsPermission: widget.gpsPermission, + jeep: _selectedJeep!, + // eta: _eta, + user: widget.user, + route: _value, + ) + ]), + ]), + )), + ], + ), + // if (widget.user != null) + // Positioned( + // bottom: Constants.defaultPadding / 2, + // right: Constants.defaultPadding / 2, + // child: CooldownButton( + // onPressed: () async { + // int result = await sendPing(widget.user!.account_email, + // widget.myLocation!, _value.routeId); + // if (result == 0) { + // widget.sendPing(true); + // } else { + // errorMessage("Failed to send your current location"); + // } + // }, + // alert: "We have broadcasted your location.", + // verified: + // widget.user!.is_verified && widget.myLocation != null, + // child: widget.myLocation != null + // ? const Icon(Icons.location_on) + // : const SizedBox( + // width: 15, + // height: 15, + // child: CircularProgressIndicator( + // color: Constants.bgColor, + // )))) + ], + ); + } +} + +class TopClipper extends CustomClipper { + @override + Rect getClip(Size size) { + return Rect.fromLTWH(0, 0, size.width, 110); // Clip to top 120 pixels + } + + @override + bool shouldReclip(covariant CustomClipper oldClipper) { + return false; + } +} diff --git a/transitrack_web/lib/components/share_live_loc/share_route_list.dart b/transitrack_web/lib/components/share_live_loc/share_route_list.dart new file mode 100644 index 0000000..d72c2d2 --- /dev/null +++ b/transitrack_web/lib/components/share_live_loc/share_route_list.dart @@ -0,0 +1,153 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:transitrack_web/components/share_live_loc/share_route_list_tile.dart'; +import 'package:transitrack_web/models/account_model.dart'; +// import 'package:transitrack_web/style/constants.dart'; +import '../../models/route_model.dart'; + +// Widget containing all active routes + +class ShareRouteList extends StatefulWidget { + final List? routes; + final int routeChoice; + final AccountData? user; + // final ValueChanged newRouteChoice; + final Function() hoverToggle; + const ShareRouteList( + {super.key, + required this.routeChoice, + required this.routes, + required this.user, + // required this.newRouteChoice, + required this.hoverToggle}); + + @override + State createState() => _ShareRouteListState(); +} + +class _ShareRouteListState extends State { + int hover = -1; + bool show_discounted = false; + + @override + void initState() { + super.initState(); + + if (widget.user != null) { + setState(() { + show_discounted = widget.user!.show_discounted; + }); + } + } + + // @override + // void didUpdateWidget(covariant ShareRouteList oldWidget) { + // super.didUpdateWidget(oldWidget); + + // if (widget.user != null && + // widget.user!.show_discounted != show_discounted) { + // setState(() { + // show_discounted = widget.user!.show_discounted; + // }); + // } + // } + + void updateBooleanField(String documentId, bool newValue) async { + // Get a reference to the Firestore collection + CollectionReference collectionReference = + FirebaseFirestore.instance.collection('accounts'); + + try { + // Get a reference to the document you want to update + DocumentReference documentReference = collectionReference.doc(documentId); + + // Update the boolean field + await documentReference.update({'show_discounted': newValue}); + + print('Boolean field updated successfully!'); + } catch (e) { + print('Error updating boolean field: $e'); + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (widget.routes != null) + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: widget.routes!.length, + itemBuilder: (context, index) { + if ((widget.routes![index].enabled) || + (widget.user != null && + widget.user!.account_type == 2 && + widget.user!.is_verified && + widget.user!.route_id == index)) { + return MouseRegion( + onExit: (_) => setState(() { + hover = -1; + }), + onHover: (_) => setState(() { + hover = index; + }), + child: GestureDetector( + onTap: () { + // widget.newRouteChoice(index); + }, + child: ShareRouteListTile( + route: widget.routes![index], + isSelected: + widget.routeChoice == index || hover == index, + hoverToggle: widget.hoverToggle, + show_discounted: show_discounted), + ), + ); + } else { + return const SizedBox(); + } + }, + ), + // if (widget.routes == null) + // const Center(child: CircularProgressIndicator()), + // Padding( + // padding: + // const EdgeInsets.symmetric(horizontal: Constants.defaultPadding), + // child: + // Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + // const Row( + // children: [ + // Text("Discounted fare"), + // IconButton( + // visualDensity: VisualDensity.compact, + // onPressed: null, + // tooltip: + // "Discounted Fare includes Student, PWD, and Senior Citizens", + // iconSize: 15, + // icon: Icon(Icons.question_mark)) + // ], + // ), + // Switch( + // activeColor: Colors.blue, + // activeTrackColor: Colors.blue.withValues(alpha: 0.5), + // inactiveThumbColor: Colors.grey, + // value: show_discounted, + // onChanged: (value) { + // setState(() { + // show_discounted = value; + // }); + + // if (widget.user != null) { + // updateBooleanField(widget.user!.account_id, value); + // } + // }, + // ), + // ]), + // ), + ], + ); + } +} diff --git a/transitrack_web/lib/components/share_live_loc/share_route_list_tile.dart b/transitrack_web/lib/components/share_live_loc/share_route_list_tile.dart new file mode 100644 index 0000000..4b618ae --- /dev/null +++ b/transitrack_web/lib/components/share_live_loc/share_route_list_tile.dart @@ -0,0 +1,62 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:flutter/material.dart'; +import '../../models/route_model.dart'; +import '../../services/format_time.dart'; +import '../../style/constants.dart'; + +// Widget containing route information + +class ShareRouteListTile extends StatefulWidget { + final RouteData route; + final bool isSelected; + final Function() hoverToggle; + final bool show_discounted; + + const ShareRouteListTile( + {super.key, + required this.route, + required this.isSelected, + required this.hoverToggle, + required this.show_discounted}); + + @override + State createState() => _ShareRouteListTileState(); +} + +class _ShareRouteListTileState extends State { + bool routeManageOpen = false; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ListTile( + isThreeLine: true, + horizontalTitleGap: 0.0, + contentPadding: const EdgeInsets.symmetric( + horizontal: Constants.defaultPadding), + title: Text(widget.route.routeName, + style: const TextStyle(color: Colors.white), + overflow: TextOverflow.ellipsis, + maxLines: 1), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(formatTime(widget.route.routeTime), + style: const TextStyle(color: Colors.white54), + overflow: TextOverflow.ellipsis), + // Text( + // "${widget.show_discounted ? widget.route.perKmRate : widget.route.routeFare} pesos", + // style: const TextStyle(color: Colors.white54), + // overflow: TextOverflow.ellipsis), + ], + ), + selectedTileColor: Colors.white10, + selected: widget.isSelected, + trailing: + Icon(Icons.circle, color: Color(widget.route.routeColor))), + ], + ); + } +} diff --git a/transitrack_web/lib/components/share_live_loc/share_selected_jeep_info.dart b/transitrack_web/lib/components/share_live_loc/share_selected_jeep_info.dart new file mode 100644 index 0000000..8413021 --- /dev/null +++ b/transitrack_web/lib/components/share_live_loc/share_selected_jeep_info.dart @@ -0,0 +1,475 @@ +import 'package:awesome_dialog/awesome_dialog.dart'; +// import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:transitrack_web/components/icon_button_big.dart'; +import 'package:transitrack_web/components/right_panel/feedback_viewer.dart'; +import 'package:transitrack_web/components/right_panel/report_form.dart'; +import 'package:transitrack_web/models/feedback_model.dart'; + +import '../../models/account_model.dart'; +import '../../models/jeep_model.dart'; +import '../../models/route_model.dart'; +import '../../config/responsive.dart'; +import '../../style/constants.dart'; +import '../right_panel/feedback_form.dart'; + +// import 'package:flutter/services.dart'; + +// This widget displays relevant information of the PUV + +class ShareSelectedJeepInfo extends StatefulWidget { + // final bool gpsPermission; + final JeepsAndDrivers jeep; + // final String? eta; + final AccountData? user; + final RouteData route; + const ShareSelectedJeepInfo( + {super.key, + // required this.gpsPermission, + required this.jeep, + // required this.eta, + required this.user, + required this.route}); + + @override + State createState() => _ShareSelectedJeepInfoState(); +} + +class _ShareSelectedJeepInfoState extends State { + late JeepsAndDrivers _jeep; + // late String? _eta; + late AccountData? _user; + late RouteData _route; + + List? jeepRating; + List? driverRating; + + bool isTapped = false; + + @override + void initState() { + super.initState(); + + setState(() { + _jeep = widget.jeep; + // _eta = widget.eta; + _user = widget.user; + _route = widget.route; + }); + + loadRatings(); + } + + @override + void didUpdateWidget(covariant ShareSelectedJeepInfo oldWidget) { + super.didUpdateWidget(oldWidget); + // if route choice changed + if (widget.jeep != _jeep) { + if (_jeep.jeep.device_id != widget.jeep.jeep.device_id) { + loadRatings(); + } + setState(() { + _jeep = widget.jeep; + }); + } + + // if (widget.eta != _eta) { + // setState(() { + // _eta = widget.eta; + // }); + // } + + if (widget.user != _user) { + setState(() { + _user = widget.user; + }); + } + + if (widget.route != _route) { + setState(() { + _route = widget.route; + }); + } + } + + void loadRatings() async { + setState(() { + driverRating = null; + jeepRating = null; + }); + var data1 = + await getRating(_jeep.driver!.account_email, 'feedback_recepient'); + var data2 = await getRating(_jeep.jeep.device_id, 'feedback_jeepney'); + + setState(() { + driverRating = data1; + jeepRating = data2; + }); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(Constants.defaultPadding), + decoration: BoxDecoration( + border: Border.all(color: Colors.white, width: 2), + borderRadius: BorderRadius.circular(Constants.defaultPadding)), + child: Responsive.isDesktop(context) + ? ShareSelectedJeepInfoBox( + // gpsPermission: widget.gpsPermission, + user: widget.user, + jeep: _jeep.jeep, + driver: _jeep.driver, + route: widget.route, + jeepRating: jeepRating, + driverRating: driverRating, + // eta: _eta + ) + : SizedBox( + height: 106, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: ShareSelectedJeepInfoBox( + // gpsPermission: widget.gpsPermission, + user: widget.user, + jeep: _jeep.jeep, + driver: _jeep.driver, + route: widget.route, + jeepRating: jeepRating, + driverRating: driverRating, + // eta: _eta + ), + ), + )); + } +} + +class ShareSelectedJeepInfoRow extends StatelessWidget { + final Widget left; + final Widget right; + const ShareSelectedJeepInfoRow( + {super.key, required this.left, required this.right}); + + @override + Widget build(BuildContext context) { + return Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + SizedBox( + height: Constants.defaultPadding * 1.5, + child: left, + ), + SizedBox( + height: Constants.defaultPadding * 1.5, + child: right, + ) + ]); + } +} + +class ShareSelectedJeepInfoBox extends StatefulWidget { + // final bool gpsPermission; + final AccountData? user; + final JeepData jeep; + final AccountData? driver; + final RouteData route; + final List? jeepRating; + final List? driverRating; + // final String? eta; + const ShareSelectedJeepInfoBox({ + super.key, + // required this.gpsPermission, + required this.user, + required this.jeep, + required this.driver, + required this.route, + required this.jeepRating, + required this.driverRating, + // required this.eta + }); + + @override + State createState() => + _ShareSelectedJeepInfoBoxState(); +} + +class _ShareSelectedJeepInfoBoxState extends State { + bool isSharing = false; + String? currentShareDocId; + + // void _toggleSharing(bool value) async { + // setState(() => isSharing = value); + + // final firestore = FirebaseFirestore.instance; + + // if (value) { + // // Create a new share doc + // final docRef = await firestore.collection('live_shares').add({ + // 'route_id': widget.route.routeId, + // 'device_id': widget.jeep.device_id, + // 'is_sharing': true, + // 'timestamp': FieldValue.serverTimestamp(), + // }); + + // final shareUrl = '${Uri.base.origin}/#/share?share_id=${docRef.id}'; + // currentShareDocId = docRef.id; + + // await Clipboard.setData(ClipboardData(text: shareUrl)); + + // if (context.mounted) { + // message('Copied Live Location Link'); + // } + // } else { + // // Stop sharing by updating Firestore + // if (currentShareDocId != null) { + // await firestore + // .collection('live_shares') + // .doc(currentShareDocId) + // .update({'is_sharing': false}); + // } + + // currentShareDocId = null; + // message('Stopped Sharing Live Location'); + // } + // } + + // void message(String message) { + // showDialog( + // context: context, + // builder: (context) { + // return AlertDialog( + // backgroundColor: Constants.bgColor, + // title: Center( + // child: Text( + // message, + // style: const TextStyle(color: Colors.white), + // ))); + // }); + // } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ShareSelectedJeepInfoRow( + left: Row( + children: [ + Icon(Icons.supervisor_account, + color: Color(widget.route.routeColor), size: 15), + const SizedBox(width: Constants.defaultPadding / 2), + const Text("Occupancy"), + ], + ), + right: Text(widget.jeep.passenger_count < 0 + ? "Available" + : widget.jeep.passenger_count >= widget.jeep.max_capacity + ? "Full" + : "${widget.jeep.passenger_count}/${widget.jeep.max_capacity}")), + const Divider(color: Colors.white), + // ShareSelectedJeepInfoRow( + // left: Row(children: [ + // Icon(Icons.timelapse_rounded, + // color: Color(widget.route.routeColor), size: 15), + // const SizedBox( + // width: Constants.defaultPadding / 2, + // ), + // const Text("ETA") + // ]), + // right: widget.gpsPermission + // ? Text(widget.eta ?? "...") + // : Row( + // children: [ + // Icon( + // Icons.location_off, + // color: Colors.red[600], + // size: 15, + // ), + // const SizedBox(width: Constants.defaultPadding / 4), + // const Text("GPS Disabled") + // ], + // )), + // const Divider(color: Colors.white), + ShareSelectedJeepInfoRow( + left: IconButton( + onPressed: () => widget.driverRating != null + ? AwesomeDialog( + context: context, + dialogType: DialogType.noHeader, + padding: const EdgeInsets.only( + left: Constants.defaultPadding, + right: Constants.defaultPadding, + bottom: Constants.defaultPadding), + width: 600, + body: PointerInterceptor( + child: FeedBackViewer( + routeData: widget.route, + isDriver: false, + feedbackRecepient: widget.jeep.device_id, + feedbacks: widget.jeepRating!))) + .show() + : null, + padding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + icon: Row(children: [ + Text(widget.jeepRating != null + ? widget.jeepRating!.isNotEmpty + ? double.parse((widget.jeepRating! + .map((e) => e.feedback_jeepney_rating) + .toList() + .reduce((value, element) => + value + element) / + widget.jeepRating!.length) + .toString()) + .toStringAsFixed(1) + : "N/A" + : "..."), + const SizedBox( + width: Constants.defaultPadding / 5, + ), + Icon(Icons.star, + color: Color(widget.route.routeColor), size: 15), + const SizedBox( + width: Constants.defaultPadding / 2, + ), + const Text("Plate Number"), + const SizedBox(width: Constants.defaultPadding / 2), + ]), + ), + right: Text(widget.jeep.device_id)), + const Divider(color: Colors.white), + ShareSelectedJeepInfoRow( + left: IconButton( + onPressed: () => widget.driverRating != null + ? AwesomeDialog( + context: context, + dialogType: DialogType.noHeader, + padding: const EdgeInsets.only( + left: Constants.defaultPadding, + right: Constants.defaultPadding, + bottom: Constants.defaultPadding), + width: 600, + body: PointerInterceptor( + child: FeedBackViewer( + routeData: widget.route, + isDriver: true, + feedbackRecepient: + widget.driver!.account_name, + feedbacks: widget.driverRating!))) + .show() + : null, + padding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + icon: Row(children: [ + Text(widget.driverRating != null + ? widget.driverRating!.isNotEmpty + ? double.parse((widget.driverRating! + .map((e) => e.feedback_driving_rating) + .toList() + .reduce((value, element) => + value + element) / + widget.driverRating!.length) + .toString()) + .toStringAsFixed(1) + : "N/A" + : "..."), + const SizedBox( + width: Constants.defaultPadding / 5, + ), + Icon(Icons.star, + color: Color(widget.route.routeColor), size: 15), + const SizedBox( + width: Constants.defaultPadding / 2, + ), + const Text("Driver"), + const SizedBox(width: Constants.defaultPadding / 2), + ]), + ), + right: Text(widget.driver!.account_name, + maxLines: 1, overflow: TextOverflow.ellipsis)), + // const Divider(color: Colors.white), + // ShareSelectedJeepInfoRow( + // left: Text("Share Live Location"), + // right: Row( + // mainAxisAlignment: MainAxisAlignment.end, + // children: [ + // IconButton( + // onPressed: () => _toggleSharing(!isSharing), + // icon: Icon( + // isSharing ? Icons.stop_circle_rounded : Icons.share, + // color: isSharing + // ? Colors.red[600] + // : Color(widget.route.routeColor), + // size: 16, + // ), + // tooltip: isSharing ? 'Stop Sharing' : 'Share Location', + // padding: EdgeInsets.zero, + // constraints: BoxConstraints(), + // ), + // ], + // ), + // ), + if (widget.user != null && + widget.user!.is_verified && + widget.driver != null) + SizedBox( + height: Constants.defaultPadding / 2, + ), + if (widget.user != null && + widget.user!.is_verified && + widget.driver != null) + Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + IconButtonBig( + color: Color(widget.route.routeColor), + function: () => AwesomeDialog( + dialogType: DialogType.noHeader, + context: (context), + width: 500, + body: PointerInterceptor( + child: FeedbackForm( + jeep: JeepsAndDrivers( + jeep: widget.jeep, driver: widget.driver), + route: widget.route, + user: widget.user)), + ).show(), + icon: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.star, color: Constants.bgColor, size: 20), + SizedBox(width: Constants.defaultPadding / 5), + Text( + "Feedback", + style: TextStyle(color: Constants.bgColor), + ) + ], + )), + const SizedBox(width: Constants.defaultPadding / 2), + IconButtonBig( + inverted: true, + color: Color(widget.route.routeColor), + function: () => AwesomeDialog( + dialogType: DialogType.noHeader, + context: (context), + width: 500, + body: PointerInterceptor( + child: ReportForm( + jeep: JeepsAndDrivers( + jeep: widget.jeep, driver: widget.driver), + route: widget.route, + user: widget.user)), + ).show(), + icon: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.report_outlined, + color: Color(widget.route.routeColor), size: 20), + const SizedBox(width: Constants.defaultPadding / 5), + Text( + "Report", + style: TextStyle(color: Color(widget.route.routeColor)), + ) + ], + )) + ]) + ], + ); + } +} diff --git a/transitrack_web/lib/components/signup_form_field.dart b/transitrack_web/lib/components/signup_form_field.dart new file mode 100644 index 0000000..1f5eaf7 --- /dev/null +++ b/transitrack_web/lib/components/signup_form_field.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import '../style/constants.dart'; + +class SignupFormField extends StatefulWidget { + final TextEditingController controller; + final String hintText; + final bool obscureText; + final TextInputType? type; + final int? lines; + final int? limit; + final bool? enabled; + final String? Function(String?)? validator; // Validator function + final FocusNode focusNode; // FocusNode for interaction tracking + + const SignupFormField({ + super.key, + required this.controller, + required this.hintText, + required this.obscureText, + required this.focusNode, + this.type, + this.lines, + this.enabled, + this.limit, + this.validator, + }); + + @override + _SignupFormFieldState createState() => _SignupFormFieldState(); +} + +class _SignupFormFieldState extends State { + String? errorText; + + @override + void initState() { + super.initState(); + + // Add a listener to the controller to validate input on text change + widget.controller.addListener(() { + validateInput(); + }); + } + + @override + void dispose() { + widget.controller.removeListener(validateInput); + super.dispose(); + } + + void validateInput() { + setState(() { + errorText = widget.validator?.call(widget.controller.text); + }); + } + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: widget.controller, + obscureText: widget.obscureText, + focusNode: widget.focusNode, + enabled: widget.enabled ?? true, + decoration: InputDecoration( + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: errorText == null && widget.controller.text.isNotEmpty + ? Colors.green // Green border for valid input + : Colors.white, // Default border color + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: errorText == null && widget.controller.text.isNotEmpty + ? Colors.green // Green border for valid input + : Constants.primaryColor, // Default focused border color + ), + ), + errorBorder: const OutlineInputBorder( + borderSide: + BorderSide(color: Constants.formError), // Red border for errors + ), + focusedErrorBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: + Constants.formError), // Red border when focused and invalid + ), + fillColor: Constants.secondaryColor, + filled: true, + hintText: widget.hintText, + hintStyle: const TextStyle(color: Colors.white), + errorText: errorText, // Display error message if invalid + ), + style: const TextStyle(color: Colors.white), + maxLines: widget.obscureText ? 1 : widget.lines, + maxLength: widget.limit, + autovalidateMode: AutovalidateMode.onUserInteraction, + ); + } +} diff --git a/transitrack_web/lib/components/text_field.dart b/transitrack_web/lib/components/text_field.dart index cb1e9aa..05a414b 100644 --- a/transitrack_web/lib/components/text_field.dart +++ b/transitrack_web/lib/components/text_field.dart @@ -11,6 +11,7 @@ class InputTextField extends StatelessWidget { final int? lines; final int? limit; final bool? enabled; + final Widget? helperWidget; InputTextField( {super.key, @@ -20,7 +21,8 @@ class InputTextField extends StatelessWidget { this.type, this.lines, this.enabled, - this.limit}); + this.limit, + this.helperWidget}); int textLength = 0; @@ -44,6 +46,7 @@ class InputTextField extends StatelessWidget { filled: true, hintText: hintText, hintStyle: const TextStyle(color: Colors.white), + helper: helperWidget, ), style: const TextStyle(color: Colors.white), maxLines: obscureText ? 1 : lines, diff --git a/transitrack_web/lib/main.dart b/transitrack_web/lib/main.dart index 139d648..5f27bde 100644 --- a/transitrack_web/lib/main.dart +++ b/transitrack_web/lib/main.dart @@ -2,15 +2,22 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; +import 'package:go_router/go_router.dart'; + import 'package:transitrack_web/MenuController.dart'; import 'package:transitrack_web/style/constants.dart'; import 'package:transitrack_web/pages/dashboard_page.dart'; +import 'package:transitrack_web/pages/share_page.dart'; import 'firebase_options.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; -// Start Here! Since JeePS is a single page app, main() calls the one and only page which is the dashboard. - +// Entry point of the Flutter web application. void main() async { WidgetsFlutterBinding.ensureInitialized(); + + setUrlStrategy(const HashUrlStrategy()); + + // Initialize Firebase with platform-specific configuration. await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); @@ -18,26 +25,54 @@ void main() async { runApp(MyApp()); } +// Routing configuration using go_router +final _router = GoRouter( + routes: [ + // Route for Dashboard Page + GoRoute( + path: '/', + builder: (context, state) => MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => MenuControllers()), + ], + child: const Dashboard(), + ), + ), + // Route for Share Page + GoRoute( + path: '/share', + builder: (context, state) { + final shareId = state.uri.queryParameters['share_id']; + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => MenuControllers()), + ], + child: SharePage(shareId: shareId), + ); + }, + ), + ], +); + +// Root widget of the application. class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { - return MaterialApp( + return MaterialApp.router( debugShowCheckedModeBanner: false, title: 'JeePS', + + // Use go_router's configuration + routerConfig: _router, + + // Global theme settings for the app theme: ThemeData.dark().copyWith( - scaffoldBackgroundColor: Constants.secondaryColor, - textTheme: GoogleFonts.poppinsTextTheme(Theme.of(context).textTheme) - .apply(bodyColor: Colors.white), - canvasColor: Constants.secondaryColor), - home: MultiProvider( - providers: [ - ChangeNotifierProvider( - create: (context) => MenuControllers(), - ), - ], - child: const Dashboard(), + scaffoldBackgroundColor: Constants.secondaryColor, + textTheme: GoogleFonts.poppinsTextTheme(Theme.of(context).textTheme) + .apply(bodyColor: Colors.white), + canvasColor: Constants.secondaryColor, ), ); } diff --git a/transitrack_web/lib/models/account_model.dart b/transitrack_web/lib/models/account_model.dart index ed9fa62..91acebb 100644 --- a/transitrack_web/lib/models/account_model.dart +++ b/transitrack_web/lib/models/account_model.dart @@ -13,6 +13,8 @@ class AccountData { String account_name; // username int account_type; // 0 for commuters, 1 for drivers, and 2 for route managers String account_id; // Generated Document ID by firestore. + bool + account_banned; // used to check if the account is banned or not for commuters bool is_verified; // Only used for Driver and Route Manager Accounts. int route_id; String? @@ -27,6 +29,7 @@ class AccountData { required this.is_verified, required this.route_id, required this.account_id, + required this.account_banned, this.jeep_driving, required this.show_discounted}); @@ -42,6 +45,7 @@ class AccountData { String? jeepDriving = data['jeep_driving']; bool showDiscounted = data['show_discounted'] ?? false; String accountId = snapshot.id; + bool accountBanned = data['account_banned']; if (accountType == 0) { isVerified = isCommuterVerified; @@ -55,7 +59,8 @@ class AccountData { route_id: routeId, jeep_driving: jeepDriving, show_discounted: showDiscounted, - account_id: accountId); + account_id: accountId, + account_banned: accountBanned); } static Future getAccountByEmail(String email) async { @@ -151,8 +156,8 @@ class AccountData { String? address; if (location != null) { - address = - await findAddress(LatLng(location.latitude, location.longitude)); + address = await findAddress( + LatLng(location.latitude, location.longitude), false); } return UsersAdditionalInfo( diff --git a/transitrack_web/lib/models/feedback_model.dart b/transitrack_web/lib/models/feedback_model.dart index 151abed..0768ded 100644 --- a/transitrack_web/lib/models/feedback_model.dart +++ b/transitrack_web/lib/models/feedback_model.dart @@ -1,5 +1,7 @@ // ignore_for_file: non_constant_identifier_names, avoid_print +import 'dart:typed_data'; + import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:transitrack_web/models/account_model.dart'; @@ -14,6 +16,7 @@ class FeedbackData { int feedback_jeepney_rating; int feedback_route; String feedback_content; + String? feedback_img; FeedbackData( {required this.feedback_sender, @@ -23,7 +26,8 @@ class FeedbackData { required this.feedback_route, required this.feedback_driving_rating, required this.feedback_content, - required this.feedback_jeepney_rating}); + required this.feedback_jeepney_rating, + this.feedback_img}); factory FeedbackData.fromFirestore(DocumentSnapshot doc) { Map data = doc.data() as Map; @@ -37,6 +41,7 @@ class FeedbackData { feedback_jeepney_rating: data['feedback_jeepney_rating'] ?? 0, feedback_content: data['feedback_content'] ?? '', feedback_route: data['feedback_route'] ?? 0, + feedback_img: data['feedback_img'] ?? '', ); } } @@ -77,3 +82,22 @@ Future?> getRating(String email, String field) async { return null; } } + +Future?> getFeedbackSender(String email) async { + try { + QuerySnapshot snapshot = await FirebaseFirestore.instance + .collection('feedbacks') + .where("feedback_sender", isEqualTo: email) + .limit(50) + .get(); + if (snapshot.docs.isNotEmpty) { + return snapshot.docs.map((e) => FeedbackData.fromFirestore(e)).toList(); + } else { + print("Error: No Feedback found"); + return []; + } + } catch (e) { + print(e.toString()); + return null; + } +} diff --git a/transitrack_web/lib/models/filter_model.dart b/transitrack_web/lib/models/filter_model.dart index 713b697..c3ed354 100644 --- a/transitrack_web/lib/models/filter_model.dart +++ b/transitrack_web/lib/models/filter_model.dart @@ -56,6 +56,12 @@ class FilterParameters { filterQueryName: "jeep_driving"), ]; + static List commutersOrderBy = [ + FilterName(filterName: "Name", filterQueryName: "account_name"), + FilterName(filterName: "Email", filterQueryName: "account_email"), + FilterName(filterName: "Status", filterQueryName: "account_banned"), + ]; + static List reportsOrderBy = [ FilterName(filterName: "Date", filterQueryName: "timestamp"), FilterName( diff --git a/transitrack_web/lib/models/jeep_model.dart b/transitrack_web/lib/models/jeep_model.dart index 990ff1c..cc02eab 100644 --- a/transitrack_web/lib/models/jeep_model.dart +++ b/transitrack_web/lib/models/jeep_model.dart @@ -1,6 +1,7 @@ // ignore_for_file: non_constant_identifier_names import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_database/firebase_database.dart'; import 'package:mapbox_gl/mapbox_gl.dart'; import '../models/account_model.dart'; @@ -9,40 +10,58 @@ import '../models/account_model.dart'; class JeepData { String device_id; Timestamp timestamp; - int passenger_count; // -1 for passive broadcasting mode + int passenger_count; int max_capacity; GeoPoint location; int route_id; - double bearing; // rotation of PUV symbol (0-360) - - JeepData( - {required this.device_id, - required this.timestamp, - required this.passenger_count, - required this.max_capacity, - required this.location, - required this.route_id, - required this.bearing}); + double bearing; // rotation of PUV icon (0-360) + + late DatabaseReference + _passengerCountRef; // reference to listen for real-time updates from realtime database + + JeepData({ + required this.device_id, + required this.timestamp, + required this.passenger_count, + required this.max_capacity, + required this.location, + required this.route_id, + required this.bearing, + }) { + _passengerCountRef = FirebaseDatabase.instance.ref( + device_id.replaceAll(" ", "_")); // realtime database does not allow " " + _listenToPassengerCountChanges(); // listener for passenger count changes in realtime database + } factory JeepData.fromSnapshot(QueryDocumentSnapshot snapshot) { Map data = snapshot.data() as Map; String deviceId = data['device_id']; Timestamp timestamp = data['timestamp']; - int passengerCount = data['passenger_count']; int maxCapacity = data['max_capacity']; GeoPoint location = data['location']; int routeId = data['route_id']; double bearing = data['bearing']; return JeepData( - device_id: deviceId, - timestamp: timestamp, - passenger_count: passengerCount, - max_capacity: maxCapacity, - location: location, - route_id: routeId, - bearing: bearing); + device_id: deviceId, + timestamp: timestamp, + passenger_count: + -1, // placeholder, will be updated from Realtime Database + max_capacity: maxCapacity, + location: location, + route_id: routeId, + bearing: bearing, + ); + } + + void _listenToPassengerCountChanges() { + // listener for passenger count changes in realtime database + _passengerCountRef.onValue.listen((event) { + if (event.snapshot.value != null) { + passenger_count = event.snapshot.value as int; + } + }); } Map toGeoJSONFeature() { @@ -94,27 +113,30 @@ class JeepHistoricalData { String driverName; bool isOperating; - JeepHistoricalData( - {required this.jeepData, - required this.driverName, - required this.isOperating}); + JeepHistoricalData({ + required this.jeepData, + required this.driverName, + required this.isOperating, + }); factory JeepHistoricalData.fromSnapshot( QueryDocumentSnapshot snapshot) { Map data = snapshot.data() as Map; return JeepHistoricalData( - jeepData: JeepData( - device_id: data['device_id'], - timestamp: data['timestamp'], - passenger_count: data['passenger_count'], - max_capacity: data['max_capacity'], - location: data['location'], - route_id: data['route_id'], - bearing: data['bearing'], - ), - driverName: data['driver'], - isOperating: data['is_operating']); + jeepData: JeepData( + device_id: data['device_id'], + timestamp: data['timestamp'], + passenger_count: + -1, // Placeholder, will be updated from Realtime Database + max_capacity: data['max_capacity'], + location: data['location'], + route_id: data['route_id'], + bearing: data['bearing'], + ), + driverName: data['driver'], + isOperating: data['is_operating'], + ); } } @@ -127,14 +149,12 @@ class PerJeepHistoricalData { Future?> getJeepHistoricalData( int routeId, DateTime day) async { - // Reference to the Firestore collection CollectionReference collectionReference = FirebaseFirestore.instance.collection('jeeps_historical'); DateTime start = day; DateTime end = day.add(const Duration(hours: 1)); - // Query all documents in the collection QuerySnapshot querySnapshot = await collectionReference .where('route_id', isEqualTo: routeId) .where('timestamp', isGreaterThanOrEqualTo: start) @@ -142,9 +162,7 @@ Future?> getJeepHistoricalData( .orderBy('timestamp', descending: true) .get(); - // Initialize a Set to store unique device IDs Set uniqueDeviceIds = {}; - List entireJeepHistoricalData = querySnapshot.docs .map((e) => JeepHistoricalData.fromSnapshot(e)) .toList(); diff --git a/transitrack_web/lib/models/ping_model.dart b/transitrack_web/lib/models/ping_model.dart index d58de78..cf27a44 100644 --- a/transitrack_web/lib/models/ping_model.dart +++ b/transitrack_web/lib/models/ping_model.dart @@ -3,6 +3,7 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:mapbox_gl/mapbox_gl.dart'; import 'dart:html' as html; +import '../services/int_to_hex.dart'; import 'package:transitrack_web/models/route_model.dart'; @@ -35,6 +36,8 @@ class PingData { ); } + // convert ping data to GeoJSON format + // a format for encoding a variety of geographic data structures Map toGeoJSONFeature() { return { 'type': 'Feature', @@ -59,6 +62,7 @@ class PingEntity { PingEntity({required this.pingData, required this.pingCircle}); } +// converts a list of Ping Data to GeoJSON format listToGeoJSON(List pings) { List> features = pings.map((ping) => ping.toGeoJSONFeature()).toList(); @@ -71,6 +75,8 @@ listToGeoJSON(List pings) { return featureCollection; } +// converts a list of CSV data to a CSV string +// CSV - info separated by commas String convertToCsv(List> csvData) { final List> csvRows = csvData.map((row) { return row.map((cell) => '"$cell"').toList(); @@ -78,6 +84,53 @@ String convertToCsv(List> csvData) { return csvRows.map((row) => row.join(',')).join('\n'); } +// adds a GeoJSON cluster to the Mapbox map +// displays the pings +Future addGeojsonCluster( + MapboxMapController mapController, RouteData routeData) async { + // remove the existing pings to update the map with new ones + // for refreshing the pings + mapController.removeLayer("pings-circles"); + mapController.removeLayer("pings-count"); + mapController.removeSource("pings"); + + // add the new pings + // addSource provides the ping data + mapController.addSource( + "pings", + GeojsonSourceProperties( + data: listToGeoJSON([]), cluster: true, clusterRadius: 20)); + + // addLayer provides the appearance of the ping data within the map + mapController.addLayer( + "pings", + "pings-circles", + CircleLayerProperties( + circleColor: intToHexColor(routeData.routeColor), + circleOpacity: 0.5, + circleRadius: [ + Expressions.step, + [Expressions.get, 'point_count'], + // example: circle radius is 20 if point count is less than 5 + 20, + 5, + 30, + 10, + 40 + ])); + + // includes the ping count for a cluster + mapController.addLayer( + "pings", + "pings-count", + const SymbolLayerProperties( + textField: [Expressions.get, 'point_count_abbreviated'], + textFont: ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'], + textSize: 12, + )); +} + +// just allows the ping data to be downloaded within the shared locations widget for RM void downloadPingDataAsCSV(List pingDataList, RouteData routeData) { // Define CSV headers List headers = [ diff --git a/transitrack_web/lib/models/report_model.dart b/transitrack_web/lib/models/report_model.dart index d355aaf..1ed8445 100644 --- a/transitrack_web/lib/models/report_model.dart +++ b/transitrack_web/lib/models/report_model.dart @@ -16,6 +16,7 @@ class ReportData { int report_type; // 0 for lost items, 1 for crime incidents, 2 for mechanical failure, 3 for accidents, 4 for other concerns GeoPoint report_location; int report_route; + String? report_img; ReportData( {required this.report_id, @@ -26,7 +27,8 @@ class ReportData { required this.report_content, required this.report_type, required this.report_route, - required this.report_location}); + required this.report_location, + this.report_img}); factory ReportData.fromFirestore(DocumentSnapshot doc) { Map data = doc.data() as Map; @@ -41,6 +43,7 @@ class ReportData { report_type: data['report_type'] ?? 0, report_route: data['report_route'], report_location: data['report_location'], + report_img: data['report_img'] ?? '', ); } @@ -52,6 +55,13 @@ class ReportData { 'Other Concerns': 4, }; + // Method to get the report type as a string + String getReportType() { + return reportTypeMap.entries + .firstWhere((entry) => entry.value == report_type) + .key; + } + static List reportDetails = [ ReportDetails(reportType: 'Lost Item', reportColors: Colors.lightBlue), ReportDetails(reportType: 'Crime Incident', reportColors: Colors.red), @@ -75,3 +85,22 @@ class ReportEntity { ReportEntity({required this.reportData, required this.reportCircle}); } + +Future?> getReportSender(String email) async { + try { + QuerySnapshot snapshot = await FirebaseFirestore.instance + .collection('reports') + .where("report_sender", isEqualTo: email) + .limit(50) + .get(); + if (snapshot.docs.isNotEmpty) { + return snapshot.docs.map((e) => ReportData.fromFirestore(e)).toList(); + } else { + // print("Error: No Feedback found"); + return []; + } + } catch (e) { + // print(e.toString()); + return null; + } +} diff --git a/transitrack_web/lib/models/route_model.dart b/transitrack_web/lib/models/route_model.dart index 6edc41e..7e37125 100644 --- a/transitrack_web/lib/models/route_model.dart +++ b/transitrack_web/lib/models/route_model.dart @@ -9,8 +9,9 @@ class RouteData { bool enabled; int routeColor; List routeCoordinates; + List stopsCoordinates; double routeFare; // Regular Fare - double routeFareDiscounted; // for PWDs, Students, Senior Citizens + double perKmRate; // for PWDs, Students, Senior Citizens int routeId; String routeName; List routeTime; @@ -21,8 +22,9 @@ class RouteData { {required this.enabled, required this.routeColor, required this.routeCoordinates, + required this.stopsCoordinates, required this.routeFare, - required this.routeFareDiscounted, + required this.perKmRate, required this.routeId, required this.routeName, required this.routeTime, @@ -36,8 +38,11 @@ class RouteData { routeCoordinates: (data['route_coordinates'] as List) .map((coord) => _parseGeoPointToLatLng(coord as GeoPoint)) .toList(), + stopsCoordinates: (data['stops_coordinates'] as List) + .map((coord) => _parseGeoPointToLatLng(coord as GeoPoint)) + .toList(), routeFare: data['route_fare'] ?? 0.0, - routeFareDiscounted: data['route_fare_discounted'] ?? 0.0, + perKmRate: data['per_km_rate'] ?? 0.0, routeId: data['route_id'] ?? 0, routeName: data['route_name'] ?? '', routeTime: (data['route_time'] as List) diff --git a/transitrack_web/lib/pages/dashboard_page.dart b/transitrack_web/lib/pages/dashboard_page.dart index a763ad6..8045791 100644 --- a/transitrack_web/lib/pages/dashboard_page.dart +++ b/transitrack_web/lib/pages/dashboard_page.dart @@ -287,11 +287,11 @@ class _DashboardState extends State { route: routeChoice, ), const SizedBox(height: Constants.defaultPadding), - MobileResearchPrompt( - pin: () => setState(() { - mobileTutorial = !mobileTutorial; - }), - ), + // MobileResearchPrompt( + // pin: () => setState(() { + // mobileTutorial = !mobileTutorial; + // }), + // ), const SizedBox(height: Constants.defaultPadding) ], ), @@ -352,7 +352,7 @@ class _DashboardState extends State { const SizedBox( height: Constants.defaultPadding, ), - const DesktopResearchPrompt() + // const DesktopResearchPrompt() ], ), ), diff --git a/transitrack_web/lib/pages/share_page.dart b/transitrack_web/lib/pages/share_page.dart new file mode 100644 index 0000000..db1b77a --- /dev/null +++ b/transitrack_web/lib/pages/share_page.dart @@ -0,0 +1,558 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:mapbox_gl/mapbox_gl.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:provider/provider.dart'; +// import 'package:transitrack_web/components/left_drawer/desktop_research_prompt.dart'; +import 'package:transitrack_web/components/left_drawer/live_test_instructions.dart'; +import 'package:transitrack_web/components/left_drawer/mobile_research_prompt.dart'; +import '../MenuController.dart'; + +import '../components/account_related/account_stream.dart'; +import '../components/header.dart'; +import '../components/left_drawer/logo.dart'; +import '../components/share_live_loc/share_map.dart'; +import '../components/share_live_loc/share_route_list.dart'; +import '../config/responsive.dart'; +import '../models/account_model.dart'; +import '../models/jeep_model.dart'; +import '../models/route_model.dart'; +import '../style/constants.dart'; +import 'package:go_router/go_router.dart' as go; + +// The one and only page of the app. This .dart file includes stream set up for the user account, routes, and puvs. + +class SharePage extends StatefulWidget { + final String? shareId; + + const SharePage({super.key, this.shareId}); + + @override + State createState() => _SharePageState(); +} + +class _SharePageState extends State { + bool mobileTutorial = false; + bool drawerOpen = false; + bool isLoading = true; + + // Keeps track of the current FirebaseAuth information + User? currentUserAuth; + + // Keeps track of the current Firebase Firestore information + AccountData? currentUserFirestore; + + // All Enabled Routes + List _routes = []; + + // All PUVs in selected Route + List jeeps = []; + + // All Drivers that are Operating + List drivers = []; + + // Stream Listeners + late StreamSubscription userAuthStream; + late StreamSubscription userFirestoreStream; + // late StreamSubscription routesFirestoreStream; + // late StreamSubscription jeepsFirestoreStream; + // late StreamSubscription driversFirestoreStream; + late StreamSubscription liveShareListener; + + // Route Selection (unselected = -1) + int routeChoice = -1; + + // Device Location Found + LatLng? deviceLoc; + + // Ensure map is loaded + bool mapLoaded = false; + + // Ensure GPS Permission is allowed + bool gpsPermission = false; + + // Upon initialization, load the current user (if there is) and load the firestore information of this user. + // Also start listening for all enabled routes. + @override + void initState() { + super.initState(); + currentUserAuth = FirebaseAuth.instance.currentUser; + listenToUserAuth(); + // listenToRoutesFirestore(); + loadSharedRouteFromUrl(); + } + + Future loadSharedRouteFromUrl() async { + final uid = widget.shareId; + + try { + final sharedDoc = await FirebaseFirestore.instance + .collection('live_shares') + .doc(uid) + .get(); + + if (!sharedDoc.exists) return; + + final data = sharedDoc.data()!; + final int routeId = data['route_id']; + final String deviceId = data['device_id']; + // final String driverName = data['driver']; + + // Fetch the route + final routeDoc = await FirebaseFirestore.instance + .collection('routes') + .where('route_id', isEqualTo: routeId) + .limit(1) + .get(); + if (routeDoc.docs.isEmpty) return; + final RouteData sharedRoute = + RouteData.fromFirestore(routeDoc.docs.first); + + // Fetch the jeep + final jeepDoc = await FirebaseFirestore.instance + .collection('jeeps_realtime') + .where('device_id', isEqualTo: deviceId) + .limit(1) + .get(); + if (jeepDoc.docs.isEmpty) return; + final JeepData sharedJeep = JeepData.fromSnapshot(jeepDoc.docs.first); + + // Fetch the driver + final driverDoc = await FirebaseFirestore.instance + .collection('accounts') + .where('jeep_driving', isEqualTo: deviceId) + .limit(1) + .get(); + final AccountData? driver = driverDoc.docs.isNotEmpty + ? AccountData.fromSnapshot(driverDoc.docs.first) + : null; + + final JeepsAndDrivers sharedJeepAndDriver = JeepsAndDrivers( + driver: driver, + jeep: sharedJeep, + ); + + setState(() { + _routes = [sharedRoute]; // Set only the fetched route + jeeps = [sharedJeepAndDriver]; // Set only the fetched jeep + drivers = driver != null ? [driver] : []; // Set only the fetched driver + routeChoice = _routes.isNotEmpty + ? 0 + : -1; // Set routeChoice to 0 if _routes is not empty + }); + + listenToLiveShareStatus(uid!); + } catch (e) { + debugPrint("Error loading shared route: $e"); + } + } + + // If the live share is ended, clear the routes and jeeps and navigate back to home. + void listenToLiveShareStatus(String uid) { + liveShareListener = FirebaseFirestore.instance + .collection('live_shares') + .doc(uid) + .snapshots() + .listen((docSnapshot) { + if (docSnapshot.exists) { + final data = docSnapshot.data() as Map; + final isSharing = data['is_sharing']; + if (!isSharing) { + setState(() { + _routes = []; + jeeps = []; + drivers = []; + routeChoice = -1; + }); + errorMessage("The live share has ended."); + // Navigate back to home after delay + Future.delayed(const Duration(seconds: 2), () { + if (mounted) { + context.go('/'); + } + }); + } + } + }); + } + + void errorMessage(String message) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + backgroundColor: Constants.bgColor, + title: Center( + child: Text( + message, + style: const TextStyle(color: Colors.white), + ))); + }); + } + + void hovering() { + setState(() { + drawerOpen = !drawerOpen; + }); + } + + // void switchRoute(int choice) { + // if (routeChoice != -1) { + // jeepsFirestoreStream.cancel(); + // driversFirestoreStream.cancel(); + // } + + // setState(() { + // routeChoice = choice; + // }); + + // if (routeChoice != -1) { + // drivers.clear(); + // jeeps.clear(); + // listenToDriversFirestore(); + // listenToJeepsFirestore(); + // } else { + // jeepsFirestoreStream.cancel(); + // driversFirestoreStream.cancel(); + // } + // } + + // void listenToDriversFirestore() { + // driversFirestoreStream = FirebaseFirestore.instance + // .collection('accounts') + // .where('account_type', isEqualTo: 1) + // .orderBy('account_email') + // .snapshots() + // .listen((QuerySnapshot snapshot) { + // if (snapshot.docs.isNotEmpty) { + // setState(() { + // drivers = snapshot.docs + // .map((doc) => AccountData.fromSnapshot(doc)) + // .toList(); + // }); + + // List collected = []; + + // for (JeepsAndDrivers jeep in jeeps) { + // bool found = drivers + // .any((driver) => driver!.jeep_driving == jeep.jeep.device_id); + // collected.add(JeepsAndDrivers( + // driver: found + // ? drivers.firstWhere( + // (driver) => driver!.jeep_driving == jeep.jeep.device_id) + // : null, + // jeep: jeep.jeep)); + // } + // setState(() { + // jeeps = collected; + // }); + // } + // }); + // } + + void listenToUserAuth() async { + userAuthStream = FirebaseAuth.instance.authStateChanges().listen( + (user) { + setState(() { + currentUserAuth = user; + }); + + if (currentUserAuth != null) { + listenToUserFirestore(); + loadSharedRouteFromUrl(); + } else { + loadSharedRouteFromUrl(); + setState(() { + currentUserFirestore = null; + isLoading = false; + }); + } + + if (routeChoice != -1) { + // switchRoute(-1); + routeChoice = -1; + } + }, + ); + } + + void listenToUserFirestore() { + userFirestoreStream = FirebaseFirestore.instance + .collection('accounts') + .where('account_email', isEqualTo: currentUserAuth?.email!) + .snapshots() + .listen((QuerySnapshot snapshot) { + if (snapshot.docs.isNotEmpty) { + setState(() { + currentUserFirestore = AccountData.fromSnapshot(snapshot.docs.first, + isCommuterVerified: currentUserAuth!.emailVerified); + isLoading = false; + }); + } + }); + } + + // void listenToRoutesFirestore() { + // routesFirestoreStream = FirebaseFirestore.instance + // .collection('routes') + // .orderBy('route_id') + // .snapshots() + // .listen((QuerySnapshot snapshot) { + // if (snapshot.docs.isNotEmpty) { + // setState(() { + // _routes = + // snapshot.docs.map((doc) => RouteData.fromFirestore(doc)).toList(); + // }); + // } + // }); + // } + + // void listenToJeepsFirestore() { + // jeepsFirestoreStream = FirebaseFirestore.instance + // .collection('jeeps_realtime') + // .where('route_id', isEqualTo: routeChoice) + // .orderBy('device_id') + // .snapshots() + // .listen((QuerySnapshot snapshot) { + // if (snapshot.docs.isNotEmpty) { + // List collected = []; + // List _jeeps = []; + // setState(() { + // _jeeps = + // snapshot.docs.map((doc) => JeepData.fromSnapshot(doc)).toList(); + // }); + // for (JeepData _jeep in _jeeps) { + // bool found = + // drivers.any((driver) => driver!.jeep_driving == _jeep.device_id); + // collected.add(JeepsAndDrivers( + // driver: found + // ? drivers.firstWhere( + // (driver) => driver!.jeep_driving == _jeep.device_id) + // : null, + // jeep: _jeep)); + // } + // setState(() { + // jeeps = collected; + // }); + // } + // }); + // } + + @override + void dispose() { + userAuthStream.cancel(); + userFirestoreStream.cancel(); + // routesFirestoreStream.cancel(); + // jeepsFirestoreStream.cancel(); + // driversFirestoreStream.cancel(); + liveShareListener.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + return Scaffold( + onDrawerChanged: (isOpened) { + setState(() { + drawerOpen = isOpened; + }); + }, + key: context.read().scaffoldKey, + drawer: PointerInterceptor( + child: Drawer( + shape: const Border(), + elevation: 0.0, + child: SingleChildScrollView( + child: Column( + children: [ + const DrawerHeader(child: Logo()), + if (!mapLoaded || _routes.isEmpty) + const Padding( + padding: EdgeInsets.symmetric( + vertical: Constants.defaultPadding), + child: Center( + child: CircularProgressIndicator(), + ), + ), + if (mapLoaded && _routes.isNotEmpty) + ShareRouteList( + routeChoice: routeChoice, + routes: _routes, + user: currentUserFirestore, + // newRouteChoice: (int choice) { + // if (routeChoice == choice) { + // switchRoute(-1); + // } else { + // switchRoute(choice); + // } + // }, + hoverToggle: hovering), + Container( + padding: const EdgeInsets.symmetric( + horizontal: Constants.defaultPadding), + child: const Divider()), + const SizedBox(height: Constants.defaultPadding), + AccountStream( + currentUser: currentUserAuth, + user: currentUserFirestore, + deviceLoc: deviceLoc, + admin: currentUserFirestore != null && + currentUserFirestore!.is_verified && + currentUserFirestore!.route_id >= 0 && + currentUserFirestore!.route_id < _routes.length + ? "${_routes[currentUserFirestore!.route_id].routeName} " + : "", + route: routeChoice, + ), + const SizedBox(height: Constants.defaultPadding), + MobileResearchPrompt( + pin: () => setState(() { + mobileTutorial = !mobileTutorial; + }), + ), + const SizedBox(height: Constants.defaultPadding) + ], + ), + )), + ), + body: SafeArea( + child: Row( + children: [ + if (Responsive.isDesktop(context)) + Expanded( + flex: 1, + child: Drawer( + shape: const Border(), + elevation: 0.0, + child: SingleChildScrollView( + child: Column( + children: [ + const DrawerHeader( + child: Logo(), + ), + if (!mapLoaded || _routes.isEmpty) + const Padding( + padding: EdgeInsets.symmetric( + vertical: Constants.defaultPadding), + child: Center( + child: CircularProgressIndicator(), + ), + ), + if (mapLoaded && _routes.isNotEmpty) + ShareRouteList( + routeChoice: routeChoice, + routes: _routes, + user: currentUserFirestore, + // newRouteChoice: (int choice) { + // if (routeChoice == choice) { + // switchRoute(-1); + // } else { + // switchRoute(choice); + // } + // }, + hoverToggle: hovering), + Container( + padding: const EdgeInsets.symmetric( + horizontal: Constants.defaultPadding), + child: const Divider()), + const SizedBox(height: Constants.defaultPadding), + AccountStream( + currentUser: currentUserAuth, + user: currentUserFirestore, + deviceLoc: deviceLoc, + admin: currentUserFirestore != null && + currentUserFirestore!.is_verified && + currentUserFirestore!.route_id >= 0 && + currentUserFirestore!.route_id < + _routes.length + ? "${_routes[currentUserFirestore!.route_id].routeName} " + : "", + route: routeChoice, + ), + // const SizedBox( + // height: Constants.defaultPadding, + // ), + // const DesktopResearchPrompt() + ], + ), + ), + ), + ), + Expanded( + flex: 5, + child: Stack(children: [ + ShareMapWidget( + route: routeChoice != -1 && routeChoice < _routes.length + ? _routes[routeChoice] + : null, + jeeps: routeChoice != -1 && routeChoice < _routes.length + ? jeeps + : null, + foundDeviceLocation: (LatLng newDeviceLocation) { + setState(() { + deviceLoc = newDeviceLocation; + }); + }, + currentUserFirestore: currentUserFirestore, + mapLoaded: (bool isLoaded) => setState(() { + mapLoaded = isLoaded; + }), + ), + if (!Responsive.isDesktop(context)) const Header(), + if (Responsive.isMobile(context) && mobileTutorial) + Positioned( + bottom: 220, + child: PointerInterceptor( + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: MediaQuery.of(context).size.width, + padding: const EdgeInsets.all( + Constants.defaultPadding), + decoration: const BoxDecoration( + color: Constants.bgColor, + ), + child: const Column( + children: [ + SizedBox( + height: 100, + child: SingleChildScrollView( + physics: + AlwaysScrollableScrollPhysics(), + child: LiveTestInstructions()), + ), + ], + )), + Positioned( + bottom: 115, + right: 0, + child: IconButton( + iconSize: 30, + visualDensity: VisualDensity.compact, + onPressed: () => setState(() { + mobileTutorial = false; + }), + icon: const Icon( + Icons.cancel, + color: Colors.red, + )), + ) + ], + ), + )) + ]), + ) + ], + ), + ), + ); + } +} diff --git a/transitrack_web/lib/services/calculate_distance.dart b/transitrack_web/lib/services/calculate_distance.dart index c92de23..6aecded 100644 --- a/transitrack_web/lib/services/calculate_distance.dart +++ b/transitrack_web/lib/services/calculate_distance.dart @@ -4,13 +4,29 @@ import 'dart:math'; // Distance calculation to find the nearest point from the device to the route coordinates double calculateDistance(LatLng point1, LatLng point2) { + const double earthRadiusKm = 6371.0; // Radius of the Earth in kilometers + double lat1 = point1.latitude; double lon1 = point1.longitude; double lat2 = point2.latitude; double lon2 = point2.longitude; - double x = (lat1 - lat2) * (lat1 - lat2); - double y = (lon1 - lon2) * (lon1 - lon2); + // Convert degrees to radians + double dLat = _degreesToRadians(lat2 - lat1); + double dLon = _degreesToRadians(lon2 - lon1); + + double a = sin(dLat / 2) * sin(dLat / 2) + + cos(_degreesToRadians(lat1)) * + cos(_degreesToRadians(lat2)) * + sin(dLon / 2) * + sin(dLon / 2); + + double c = 2 * atan2(sqrt(a), sqrt(1 - a)); + + // Distance in kilometers + return earthRadiusKm * c; +} - return sqrt(x + y); +double _degreesToRadians(double degrees) { + return degrees * pi / 180; } diff --git a/transitrack_web/lib/services/find_location.dart b/transitrack_web/lib/services/find_location.dart index c94bda6..476493a 100644 --- a/transitrack_web/lib/services/find_location.dart +++ b/transitrack_web/lib/services/find_location.dart @@ -2,21 +2,75 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:mapbox_gl/mapbox_gl.dart'; import 'package:transitrack_web/config/keys.dart'; +import '../services/calculate_distance.dart'; -// API Call Function for extracting the coordinate name. This is used in the Reporting Feature since there are -// location sensitive report types like crime, accidents, and mechanical problems +// API Call Function for extracting the coordinate name using the Search Box API +Future findAddress(LatLng latLng, bool isSpecific) async { + // Truncate latitude and longitude to 4 decimal places + String truncatedLat = latLng.latitude.toStringAsFixed(4); + String truncatedLon = latLng.longitude.toStringAsFixed(4); -Future findAddress(LatLng latLng) async { - String loc = '${latLng.longitude},${latLng.latitude}'; + // Use the Search Box API String apiUrl = - 'https://api.mapbox.com/geocoding/v5/mapbox.places/$loc.json?access_token=${Keys.MapBoxKey}'; + 'https://api.mapbox.com/search/searchbox/v1/reverse?longitude=$truncatedLon&latitude=$truncatedLat&access_token=${Keys.MapBoxKey}'; final response = await http.get(Uri.parse(apiUrl)); if (response.statusCode == 200) { final decoded = json.decode(response.body); final features = decoded['features']; - return '${features[0]['text']}, ${features[2]['text']}'; + + if (features.isNotEmpty) { + // Extract the name and address of the closest feature + final firstFeature = features[0]; + if (!isSpecific) { + String name = firstFeature['properties']['name'] ?? 'Unknown Name'; + return name; + } else { + // Initialize variables to track the closest feature + double closestDistance = double.infinity; + Map? closestFeature; + + // Find the closest feature + for (var feature in features) { + // Extract the coordinates of the feature + double featureLat = double.parse( + feature['geometry']['coordinates'][1].toStringAsFixed(4)); + double featureLon = double.parse( + feature['geometry']['coordinates'][0].toStringAsFixed(4)); + + // Truncate the input coordinates to 4 decimal places + double truncatedLat = + double.parse(latLng.latitude.toStringAsFixed(4)); + double truncatedLon = + double.parse(latLng.longitude.toStringAsFixed(4)); + + // Calculate the distance using truncated coordinates + double distance = calculateDistance( + LatLng(truncatedLat, truncatedLon), + LatLng(featureLat, featureLon), + ); + + // Update the closest feature if this one is closer + if (distance < closestDistance) { + closestDistance = distance; + closestFeature = feature; + } + } + + // If a closest feature is found, return its details + if (closestFeature != null) { + String name = closestFeature['properties']['name'] ?? 'Unknown Name'; + String address = + closestFeature['properties']['address'] ?? 'Unknown Address'; + return '$name, $address'; + } + // Fallback: If no closest feature is found + return 'No closest point found'; + } + } else { + return 'No address found'; + } } else { return 'Error: ${response.statusCode} - ${response.reasonPhrase}'; } diff --git a/transitrack_web/lib/services/mapbox/minute_old_checker.dart b/transitrack_web/lib/services/mapbox/minute_old_checker.dart new file mode 100644 index 0000000..e87b6f5 --- /dev/null +++ b/transitrack_web/lib/services/mapbox/minute_old_checker.dart @@ -0,0 +1,10 @@ +bool minuteOldChecker(DateTime timestamp) { + // Get the current time + DateTime now = DateTime.now(); + + // Calculate the difference in milliseconds + int differenceInMilliseconds = now.difference(timestamp).inMilliseconds; + + // Check if the difference is less than a minute (60,000 milliseconds) + return differenceInMilliseconds < 60000; +} diff --git a/transitrack_web/lib/style/constants.dart b/transitrack_web/lib/style/constants.dart index 9c64e48..f43d6b5 100644 --- a/transitrack_web/lib/style/constants.dart +++ b/transitrack_web/lib/style/constants.dart @@ -18,6 +18,8 @@ class Constants { static const secondaryColor = Color(0xFF2A2D3E); static const bgColor = Color(0xFF212332); + static const formError = Color.fromARGB(255, 196, 79, 71); + static const defaultPadding = 16.0; static const ikotColor = Color(0xFFFFC107); diff --git a/transitrack_web/linux/flutter/generated_plugin_registrant.cc b/transitrack_web/linux/flutter/generated_plugin_registrant.cc index 406c00c..a52086e 100644 --- a/transitrack_web/linux/flutter/generated_plugin_registrant.cc +++ b/transitrack_web/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); g_autoptr(FlPluginRegistrar) rive_common_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RivePlugin"); rive_plugin_register_with_registrar(rive_common_registrar); diff --git a/transitrack_web/linux/flutter/generated_plugins.cmake b/transitrack_web/linux/flutter/generated_plugins.cmake index 2379397..c500f2a 100644 --- a/transitrack_web/linux/flutter/generated_plugins.cmake +++ b/transitrack_web/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux rive_common url_launcher_linux ) diff --git a/transitrack_web/macos/Flutter/GeneratedPluginRegistrant.swift b/transitrack_web/macos/Flutter/GeneratedPluginRegistrant.swift index 75b0854..74af3a8 100644 --- a/transitrack_web/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/transitrack_web/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,10 +6,13 @@ import FlutterMacOS import Foundation import cloud_firestore +import cloud_functions import connectivity_plus +import file_selector_macos import firebase_auth import firebase_core import firebase_database +import firebase_storage import geolocator_apple import google_sign_in_ios import path_provider_foundation @@ -18,10 +21,13 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) + FLTFirebaseFunctionsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFunctionsPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin")) + FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/transitrack_web/pubspec.lock b/transitrack_web/pubspec.lock index 6da1bc3..9e8b048 100644 --- a/transitrack_web/pubspec.lock +++ b/transitrack_web/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: e4f2a7ef31b0ab2c89d2bde35ef3e6e6aff1dce5e66069c6540b0e9cfe33ee6b + sha256: de9ecbb3ddafd446095f7e833c853aff2fa1682b017921fe63a833f9d6f0e422 url: "https://pub.dev" source: hosted - version: "1.3.50" + version: "1.3.54" animated_stack_widget: dependency: transitive description: @@ -21,18 +21,18 @@ packages: dependency: transitive description: name: archive - sha256: "6199c74e3db4fbfbd04f66d739e72fe11c8a8957d5f219f1f4482dbde6420b5a" + sha256: "7dcbd0f87fe5f61cb28da39a1a8b70dbc106e2fe0516f7836eb7bb2948481a12" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.5" args: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" async: dependency: transitive description: @@ -85,26 +85,50 @@ packages: dependency: "direct main" description: name: cloud_firestore - sha256: "9ba2379f319906567f7078ebf086951c4e333c71620455084c18258f267f0636" + sha256: "89a5e32716794b6a8d0ec1b5dfda988194e92daedaa3f3bed66fa0d0a595252e" url: "https://pub.dev" source: hosted - version: "5.6.2" + version: "5.6.6" cloud_firestore_platform_interface: dependency: transitive description: name: cloud_firestore_platform_interface - sha256: c3a2987addea08273c582a91f5fb173ca81916ef6d7f8e1a6760c3a8a3a53fc7 + sha256: "9f012844eb59be6827ed97415875c5a29ccacd28bc79bf85b4680738251a33df" url: "https://pub.dev" source: hosted - version: "6.6.2" + version: "6.6.6" cloud_firestore_web: dependency: transitive description: name: cloud_firestore_web - sha256: "4c1bc404d825c68153660b12fd937b90b75cf3aa622cc077da5308ccaec17a9e" + sha256: b8b754269be0e907acd9ff63ad60f66b84c78d330ca1d7e474f86c9527ddc803 url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.4.6" + cloud_functions: + dependency: "direct main" + description: + name: cloud_functions + sha256: "91c87991b340975b734e17a777c8953a466651d1621b95a9be1220060f33f9ee" + url: "https://pub.dev" + source: hosted + version: "5.4.0" + cloud_functions_platform_interface: + dependency: transitive + description: + name: cloud_functions_platform_interface + sha256: "816ff83f385356ca750b571c9b3b79f1c4aadfb94629c1bc87fb4d48d7ac1c13" + url: "https://pub.dev" + source: hosted + version: "5.6.5" + cloud_functions_web: + dependency: transitive + description: + name: cloud_functions_web + sha256: "75b228748b141ebf02ac724dcf4f6dcc0cb493ca10350d8402ffdb2da2f63f5e" + url: "https://pub.dev" + source: hosted + version: "4.10.11" collection: dependency: transitive description: @@ -117,10 +141,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412" + sha256: "04bf81bb0b77de31557b58d052b24b3eee33f09a6e7a8c68a3e247c7df19ec27" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.3" connectivity_plus_platform_interface: dependency: transitive description: @@ -129,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" crypto: dependency: transitive description: @@ -201,38 +233,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + url: "https://pub.dev" + source: hosted + version: "0.9.3+4" firebase_auth: dependency: "direct main" description: name: firebase_auth - sha256: eabb0dddb72956b7004aabf03c0bf650c19f92e470441d6a4e6b19cd4e9ca958 + sha256: "54c62b2d187709114dd09ce658a8803ee91f9119b0e0d3fc2245130ad9bff9ad" url: "https://pub.dev" source: hosted - version: "5.4.1" + version: "5.5.2" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface - sha256: b8684ade236c20638a17343f89029f7059fef13245cc102413d5c9cf6f98afb5 + sha256: "5402d13f4bb7f29f2fb819f3b6b5a5a56c9f714aef2276546d397e25ac1b6b8e" url: "https://pub.dev" source: hosted - version: "7.5.1" + version: "7.6.2" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: "5ea5f2fbe02900ff2772ed9ea95ed8b1c7e160f4b4b6000d227bdeecd40904f8" + sha256: "2be496911f0807895d5fe8067b70b7d758142dd7fb26485cbe23e525e2547764" url: "https://pub.dev" source: hosted - version: "5.13.7" + version: "5.14.2" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: d851c1ca98fd5a4c07c747f8c65dacc2edd84a4d9ac055d32a5f0342529069f5 + sha256: "017d17d9915670e6117497e640b2859e0b868026ea36bf3a57feb28c3b97debe" url: "https://pub.dev" source: hosted - version: "3.10.1" + version: "3.13.0" firebase_core_platform_interface: dependency: transitive description: @@ -245,34 +309,58 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: fbc008cf390d909b823763064b63afefe9f02d8afdb13eb3f485b871afee956b + sha256: "129a34d1e0fb62e2b488d988a1fc26cc15636357e50944ffee2862efe8929b23" url: "https://pub.dev" source: hosted - version: "2.19.0" + version: "2.22.0" firebase_database: dependency: "direct main" description: name: firebase_database - sha256: "4b4d1948669aff403a98af6ceb2537b481b8874cadb3f576a62223119e4be374" + sha256: "182ce4713d47ffc5f19a5a7b934867d1fae9c33081febcec8c062cb89fc14652" url: "https://pub.dev" source: hosted - version: "11.3.1" + version: "11.3.5" firebase_database_platform_interface: dependency: transitive description: name: firebase_database_platform_interface - sha256: a906e20e398efd7cde9e3b4c23a868c3f54ee6a58c0c115e9340d9ed064df1a4 + sha256: b65f416dd2c8ac2d5322241e5411a24ed3da43d0f38aaf9ab6c211d72e52261b url: "https://pub.dev" source: hosted - version: "0.2.6+1" + version: "0.2.6+5" firebase_database_web: dependency: transitive description: name: firebase_database_web - sha256: f44b7032430f4f7e65dd67241ba4a6c0aa16b7fe37a5526a24a0ebc4e58c079a + sha256: "5203141fe00a1edfaed5f8e0444b8e4ef807a8ec6eca925621b1cab69b6c06e4" url: "https://pub.dev" source: hosted - version: "0.2.6+7" + version: "0.2.6+11" + firebase_storage: + dependency: "direct main" + description: + name: firebase_storage + sha256: b66435730252985c49aabe83e0490bcfab2e7b3f2192bf421ca596fa490de14a + url: "https://pub.dev" + source: hosted + version: "12.4.5" + firebase_storage_platform_interface: + dependency: transitive + description: + name: firebase_storage_platform_interface + sha256: "08d32cae58200c34f504098d106952213f2e4c32db111ae7757a86887428ab81" + url: "https://pub.dev" + source: hosted + version: "5.2.5" + firebase_storage_web: + dependency: transitive + description: + name: firebase_storage_web + sha256: d6aee6867f8c369a88484367a8b1f0f6d0f022b4ff2622b51e32dfeef839f9d4 + url: "https://pub.dev" + source: hosted + version: "3.10.12" fixnum: dependency: transitive description: @@ -342,6 +430,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3" + url: "https://pub.dev" + source: hosted + version: "2.0.27" flutter_svg: dependency: "direct main" description: @@ -364,7 +460,7 @@ packages: source: sdk version: "0.0.0" flutter_web_plugins: - dependency: transitive + dependency: "direct main" description: flutter source: sdk version: "0.0.0" @@ -372,50 +468,58 @@ packages: dependency: "direct main" description: name: geolocator - sha256: d2ec66329cab29cb297d51d96c067d457ca519dca8589665fa0b82ebacb7dbe4 + sha256: f62bcd90459e63210bbf9c35deb6a51c521f992a78de19a1fe5c11704f9530e2 url: "https://pub.dev" source: hosted - version: "13.0.2" + version: "13.0.4" geolocator_android: dependency: transitive description: name: geolocator_android - sha256: "7aefc530db47d90d0580b552df3242440a10fe60814496a979aa67aa98b1fd47" + sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d url: "https://pub.dev" source: hosted - version: "4.6.1" + version: "4.6.2" geolocator_apple: dependency: transitive description: name: geolocator_apple - sha256: "6154ea2682563f69fc0125762ed7e91e7ed85d0b9776595653be33918e064807" + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 url: "https://pub.dev" source: hosted - version: "2.3.8+1" + version: "2.3.13" geolocator_platform_interface: dependency: transitive description: name: geolocator_platform_interface - sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012" + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "4.2.6" geolocator_web: dependency: transitive description: name: geolocator_web - sha256: "2ed69328e05cd94e7eb48bb0535f5fc0c0c44d1c4fa1e9737267484d05c29b5e" + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.3" geolocator_windows: dependency: transitive description: name: geolocator_windows - sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e" + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" url: "https://pub.dev" source: hosted - version: "0.2.3" + version: "0.2.5" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: "4cdfcc6a178632d1dbb7a728f8e84a1466211354704b9cdc03eee661d3277732" + url: "https://pub.dev" + source: hosted + version: "15.0.0" google_fonts: dependency: "direct main" description: @@ -436,42 +540,42 @@ packages: dependency: "direct main" description: name: google_sign_in - sha256: fad6ddc80c427b0bba705f2116204ce1173e09cf299f85e053d57a55e5b2dd56 + sha256: d0a2c3bcb06e607bb11e4daca48bd4b6120f0bbc4015ccebbe757d24ea60ed2a url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.0" google_sign_in_android: dependency: transitive description: name: google_sign_in_android - sha256: "3b96f9b6cf61915f73cbe1218a192623e296a9b8b31965702503649477761e36" + sha256: "4e52c64366bdb3fe758f683b088ee514cc7a95e69c52b5ee9fc5919e1683d21b" url: "https://pub.dev" source: hosted - version: "6.1.34" + version: "6.2.0" google_sign_in_ios: dependency: transitive description: name: google_sign_in_ios - sha256: "83f015169102df1ab2905cf8abd8934e28f87db9ace7a5fa676998842fed228a" + sha256: "29cd125f58f50ceb40e8253d3c0209e321eee3e5df16cd6d262495f7cad6a2bd" url: "https://pub.dev" source: hosted - version: "5.7.8" + version: "5.8.1" google_sign_in_platform_interface: dependency: transitive description: name: google_sign_in_platform_interface - sha256: "1f6e5787d7a120cc0359ddf315c92309069171306242e181c09472d1b00a2971" + sha256: "5f6f79cf139c197261adb6ac024577518ae48fdff8e53205c5373b5f6430a8aa" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.5.0" google_sign_in_web: dependency: transitive description: name: google_sign_in_web - sha256: ada595df6c30cead48e66b1f3a050edf0c5cf2ba60c185d69690e08adcc6281b + sha256: "460547beb4962b7623ac0fb8122d6b8268c951cf0b646dd150d60498430e4ded" url: "https://pub.dev" source: hosted - version: "0.12.4+3" + version: "0.12.4+4" graphs: dependency: transitive description: @@ -508,10 +612,74 @@ packages: dependency: transitive description: name: image - sha256: "8346ad4b5173924b5ddddab782fc7d8a6300178c8b1dc427775405a01701c4a6" + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "8bd392ba8b0c8957a157ae0dc9fcf48c58e6c20908d5880aea1d79734df090e9" + url: "https://pub.dev" + source: hosted + version: "0.8.12+22" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" + url: "https://pub.dev" + source: hosted + version: "0.8.12+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" + url: "https://pub.dev" + source: hosted + version: "2.10.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "0.2.1+1" intl: dependency: "direct main" description: @@ -584,12 +752,20 @@ packages: url: "https://pub.dev" source: hosted version: "2.5.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" mapbox_gl: dependency: "direct main" description: path: "." ref: HEAD - resolved-ref: d4aee363c31ea92b6c79ac569d3e0984686411e3 + resolved-ref: "4a09cc4401bb77e900f005cc1e2f712193b997af" url: "https://github.com/flutter-mapbox-gl/maps" source: git version: "0.16.0" @@ -606,7 +782,7 @@ packages: description: path: mapbox_gl_platform_interface ref: HEAD - resolved-ref: d4aee363c31ea92b6c79ac569d3e0984686411e3 + resolved-ref: "4a09cc4401bb77e900f005cc1e2f712193b997af" url: "https://github.com/tobrun/flutter-mapbox-gl.git" source: git version: "0.16.0" @@ -615,7 +791,7 @@ packages: description: path: mapbox_gl_web ref: HEAD - resolved-ref: d4aee363c31ea92b6c79ac569d3e0984686411e3 + resolved-ref: "4a09cc4401bb77e900f005cc1e2f712193b997af" url: "https://github.com/tobrun/flutter-mapbox-gl.git" source: git version: "0.16.0" @@ -651,6 +827,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" nested: dependency: transitive description: @@ -695,10 +879,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.16" path_provider_foundation: dependency: transitive description: @@ -735,26 +919,26 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" url: "https://pub.dev" source: hosted - version: "11.3.1" + version: "11.4.0" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1" + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc url: "https://pub.dev" source: hosted - version: "12.0.13" + version: "12.1.0" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 + sha256: f84a188e79a35c687c132a0a0556c254747a08561e99ab933f12f6ca71ef3c98 url: "https://pub.dev" source: hosted - version: "9.4.5" + version: "9.4.6" permission_handler_html: dependency: transitive description: @@ -767,10 +951,10 @@ packages: dependency: transitive description: name: permission_handler_platform_interface - sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 url: "https://pub.dev" source: hosted - version: "4.2.3" + version: "4.3.0" permission_handler_windows: dependency: transitive description: @@ -863,10 +1047,10 @@ packages: dependency: "direct main" description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "489024f942069c2920c844ee18bb3d467c69e48955a4f32d1677f71be103e310" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.4" rive: dependency: transitive description: @@ -1020,18 +1204,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" + sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4" url: "https://pub.dev" source: hosted - version: "6.3.14" + version: "6.3.15" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" url_launcher_linux: dependency: transitive description: @@ -1084,10 +1268,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" + sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" url: "https://pub.dev" source: hosted - version: "1.1.15" + version: "1.1.18" vector_graphics_codec: dependency: transitive description: @@ -1124,10 +1308,10 @@ packages: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" wkt_parser: dependency: transitive description: diff --git a/transitrack_web/pubspec.yaml b/transitrack_web/pubspec.yaml index 156e0f9..7723263 100644 --- a/transitrack_web/pubspec.yaml +++ b/transitrack_web/pubspec.yaml @@ -28,57 +28,53 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: - flutter: - sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.5 - provider: ^6.0.5 - firebase_core: ^3.10.1 - flutter_svg: ^2.0.5 + awesome_dialog: ^3.1.0 cloud_firestore: ^5.6.2 - mapbox_gl: - git: - url: https://github.com/flutter-mapbox-gl/maps - google_fonts: ^6.2.1 - fl_chart: ^0.70.2 - flutter_switch: ^0.3.2 - firebase_database: ^11.3.1 + cloud_functions: ^5.3.3 connectivity_plus: ^6.1.2 - flutter_map_marker_cluster: ^1.1.0 csv: ^6.0.0 - path_provider: ^2.0.15 - intl: ^0.20.1 - universal_html: ^2.2.2 - awesome_dialog: ^3.1.0 - url_launcher: ^6.2.6 + cupertino_icons: ^1.0.5 firebase_auth: ^5.4.1 - google_sign_in: ^6.2.1 - geolocator: ^13.0.2 + firebase_core: ^3.10.1 + firebase_database: ^11.3.1 + firebase_storage: ^12.4.2 + fl_chart: ^0.70.2 + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter flutter_colorpicker: ^1.0.3 flutter_dotenv: ^5.1.0 + flutter_map_marker_cluster: ^1.1.0 + flutter_svg: ^2.0.5 + flutter_switch: ^0.3.2 + geolocator: ^13.0.2 + go_router: ^15.0.0 + google_fonts: ^6.2.1 + google_sign_in: ^6.2.1 http: ^1.2.0 - shimmer_effect: ^1.0.4 - shimmer: ^3.0.0 - pointer_interceptor: ^0.10.1 + image_picker: ^1.1.2 + intl: ^0.20.1 + mapbox_gl: + git: + url: https://github.com/flutter-mapbox-gl/maps + path_provider: ^2.0.15 permission_handler: ^11.3.1 + pointer_interceptor: ^0.10.1 + provider: ^6.0.5 + shimmer: ^3.0.0 + shimmer_effect: ^1.0.4 table_calendar: ^3.0.9 + universal_html: ^2.2.2 + url_launcher: ^6.3.1 dev_dependencies: + flutter_lints: ^5.0.0 flutter_test: sdk: flutter - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^5.0.0 - # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec - # The following section is specific to Flutter packages. flutter: # The following line ensures that the Material Icons font is @@ -92,10 +88,8 @@ flutter: # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware - # For details regarding adding assets from package dependencies, see # https://flutter.dev/assets-and-images/#from-packages - # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a @@ -112,7 +106,6 @@ flutter: weight: 200 - asset: fonts/Roboto-Regular.ttf weight: 100 - # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro diff --git a/transitrack_web/windows/flutter/generated_plugin_registrant.cc b/transitrack_web/windows/flutter/generated_plugin_registrant.cc index 7973aa7..f674101 100644 --- a/transitrack_web/windows/flutter/generated_plugin_registrant.cc +++ b/transitrack_web/windows/flutter/generated_plugin_registrant.cc @@ -8,8 +8,10 @@ #include #include +#include #include #include +#include #include #include #include @@ -20,10 +22,14 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); FirebaseAuthPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + FirebaseStoragePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseStoragePluginCApi")); GeolocatorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("GeolocatorWindows")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/transitrack_web/windows/flutter/generated_plugins.cmake b/transitrack_web/windows/flutter/generated_plugins.cmake index 03a11f3..54e7a1b 100644 --- a/transitrack_web/windows/flutter/generated_plugins.cmake +++ b/transitrack_web/windows/flutter/generated_plugins.cmake @@ -5,8 +5,10 @@ list(APPEND FLUTTER_PLUGIN_LIST cloud_firestore connectivity_plus + file_selector_windows firebase_auth firebase_core + firebase_storage geolocator_windows permission_handler_windows rive_common