diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bb5b7be..c2d0ae9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,7 +24,8 @@ concurrency: #Allow repo secrets env: - VITE_HERE_API_KEY: ${{ secrets.VITE_HERE_API_KEY }} + VITE_BACKEND_URL: "https://web-production-4659.up.railway.app" + VITE_WEATHER_API_KEY: ${{ secrets.VITE_WEATHER_API_KEY }} # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: diff --git a/.gitignore b/.gitignore index f6b98c7..74b1b24 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,12 @@ +# Root .gitignore + # dependencies /node_modules package-lock.json # production /build -/dist -dev-dist/ +/dev-dist/ # misc .bak diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..bef5264 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: npm run start \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..69a9d17 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# 🌟 GlowPath + +GlowPath is a progressive web application designed to enhance user safety by enabling **real-time location sharing** that can be shared easily via chat, and providing instant alerts to contacts or authorities via an **SOS button**. + +--- + +## πŸš€ Key Features: +- **Real-Time Location Sharing**: Share your live location with a trusted contact list. +- **SOS Button**: Notify contacts and authorities during emergencies with a personalized emergency button. +- **Safe Zone Mapping**: View nearby safe zones like police stations and hospitals on an interactive map. +- **Safety Analytics**: Visualize important safety statistics for better decision-making. + +--- + +## πŸ› οΈ Getting Started: +GlowPath is live! Visit the live version at https://glowpathorg.github.io/GlowPath/ + + +To run GlowPath locally, ensure the following dependencies and accounts are set up: +1. **Node.js**: [Download and install Node.js](https://nodejs.org). +2. **MongoDB**: Set up a local or cloud-based MongoDB instance (e.g., [MongoDB Atlas](https://www.mongodb.com/atlas)). +3. **API Keys**: + - [HERE API](https://developer.here.com) + - [Weather API](https://www.weatherapi.com/) + - [Twilio](https://www.twilio.com/) for sending emergency messages. +4. **OpenStreetMap and Overpass API**: No additional setup needed. + +--- + +## πŸ“¦ Installation: + +### 1. Clone the repository: + +git clone +cd GlowPath + +### 2. Install dependencies: +-root Directory +npm install + +- server: + cd server + npm install + +- client: +cd client +npm install + +### 3. Set up environment variables: +Create a .env file in the client for the weather API Key +Create a .env file in the server directory and add the following: +SERVER_PORT=3002 +CLIENT_PORT=5173 +MONGODB_URI=your_mongodb_connection_string +JWT_SECRET=your_secret_key +TWILIO_ACCOUNT_SID +TWILIO_AUTH_TOKEN +TWILIO_PHONE_NUMBER + + +## πŸ’» Tech Stack +β€’ Frontend: React.js and Typescript +β€’ Backend: Node.js with Express.js for routing and API logic. +β€’ Database: MongoDB for storing data. +β€’ Authentication: JSON Web Tokens (JWT) for secure user sessions. +β€’ Mapping: Leaflet.js for an interactive map. +β€’ APIs and Tools: +HERE API +Nominatim +Twilio +Openstreetmaps +Overpass +Weather API +β€’ Others: Axios. + + +## 🀝 Contributing: + +Contributions, issues, and feature requests are welcome! +Feel free to check the issues page or submit a pull request. + +## Authors: +- Hadil Ben Koura - linkedIn:www.linkedin.com/in/hadil-benkoura +- Mellissa Cessna - linkedIn: www.linkedin.com/in/cessna +- Jonas Rinderlin - linkedIn: www.linkedin.com/in/ + +Enjoy using GlowPath, and stay safe! πŸšΆβ€β™‚οΈπŸ›‘οΈ + diff --git a/client/eslint.config.js b/client/eslint.config.js index 092408a..82c2e20 100644 --- a/client/eslint.config.js +++ b/client/eslint.config.js @@ -1,8 +1,8 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; export default tseslint.config( { ignores: ['dist'] }, @@ -24,5 +24,5 @@ export default tseslint.config( { allowConstantExport: true }, ], }, - }, -) + } +); diff --git a/client/index.html b/client/index.html index a8188a7..c536cb6 100644 --- a/client/index.html +++ b/client/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + GlowPath
diff --git a/client/package-lock.json b/client/package-lock.json index 9794af3..af91c2e 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,19 +12,25 @@ "@types/leaflet-rotatedmarker": "^0.2.5", "axios": "^1.7.7", "boundingbox": "^1.4.0", + "date-fns": "^4.1.0", "leaflet": "^1.9.4", "leaflet-polylinedecorator": "^1.6.0", "leaflet-rotatedmarker": "^0.2.0", "mapbox-gl-leaflet": "^0.0.16", "react": "^18.3.1", + "react-chartjs-2": "^5.2.0", "react-dom": "^18.3.1", + "react-icons": "^5.4.0", "react-leaflet": "^4.2.1", + "react-phone-number-input": "^3.4.9", "react-router-dom": "^7.0.1", + "recharts": "^2.14.1", "socket.io-client": "^4.8.1" }, "devDependencies": { "@eslint/js": "^9.13.0", "@types/leaflet": "^1.9.14", + "@types/node": "^22.10.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/serviceworker": "^0.0.106", @@ -35,8 +41,8 @@ "globals": "^15.11.0", "typescript": "~5.6.2", "typescript-eslint": "^8.11.0", - "vite": "^5.4.10", - "vite-node": "^2.1.5", + "vite": "^6.3.5", + "vite-node": "^3.1.3", "vite-plugin-pwa": "^0.21.0" } }, @@ -55,15 +61,15 @@ } }, "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==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -362,9 +368,9 @@ } }, "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==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -372,9 +378,9 @@ } }, "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==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, "license": "MIT", "engines": { @@ -407,27 +413,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", - "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.0" + "@babel/types": "^7.27.1" }, "bin": { "parser": "bin/babel-parser.js" @@ -1563,28 +1569,24 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", - "dev": true, + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1620,23 +1622,23 @@ } }, "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", "cpu": [ "ppc64" ], @@ -1647,13 +1649,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", "cpu": [ "arm" ], @@ -1664,13 +1666,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", "cpu": [ "arm64" ], @@ -1681,13 +1683,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", "cpu": [ "x64" ], @@ -1698,13 +1700,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", "cpu": [ "arm64" ], @@ -1715,13 +1717,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", "cpu": [ "x64" ], @@ -1732,13 +1734,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", "cpu": [ "arm64" ], @@ -1749,13 +1751,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", "cpu": [ "x64" ], @@ -1766,13 +1768,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", "cpu": [ "arm" ], @@ -1783,13 +1785,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", "cpu": [ "arm64" ], @@ -1800,13 +1802,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", "cpu": [ "ia32" ], @@ -1817,13 +1819,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", "cpu": [ "loong64" ], @@ -1834,13 +1836,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", "cpu": [ "mips64el" ], @@ -1851,13 +1853,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", "cpu": [ "ppc64" ], @@ -1868,13 +1870,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", "cpu": [ "riscv64" ], @@ -1885,13 +1887,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", "cpu": [ "s390x" ], @@ -1902,13 +1904,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", "cpu": [ "x64" ], @@ -1919,13 +1921,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", "cpu": [ "x64" ], @@ -1936,13 +1955,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", "cpu": [ "x64" ], @@ -1953,13 +1989,13 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", "cpu": [ "x64" ], @@ -1970,13 +2006,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", "cpu": [ "arm64" ], @@ -1987,13 +2023,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", "cpu": [ "ia32" ], @@ -2004,13 +2040,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", "cpu": [ "x64" ], @@ -2021,7 +2057,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -2129,9 +2165,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", - "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", + "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", "dev": true, "license": "MIT", "engines": { @@ -2297,6 +2333,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT", + "peer": true + }, "node_modules/@mapbox/jsonlint-lines-primitives": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", @@ -2488,9 +2531,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.27.4.tgz", - "integrity": "sha512-2Y3JT6f5MrQkICUyRVCw4oa0sutfAsgaSsb0Lmmy1Wi2y7X5vT9Euqw4gOsCyy0YfKURBg35nhUKZS4mDcfULw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", + "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", "cpu": [ "arm" ], @@ -2502,9 +2545,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.27.4.tgz", - "integrity": "sha512-wzKRQXISyi9UdCVRqEd0H4cMpzvHYt1f/C3CoIjES6cG++RHKhrBj2+29nPF0IB5kpy9MS71vs07fvrNGAl/iA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", + "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", "cpu": [ "arm64" ], @@ -2516,9 +2559,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.27.4.tgz", - "integrity": "sha512-PlNiRQapift4LNS8DPUHuDX/IdXiLjf8mc5vdEmUR0fF/pyy2qWwzdLjB+iZquGr8LuN4LnUoSEvKRwjSVYz3Q==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", + "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", "cpu": [ "arm64" ], @@ -2530,9 +2573,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.27.4.tgz", - "integrity": "sha512-o9bH2dbdgBDJaXWJCDTNDYa171ACUdzpxSZt+u/AAeQ20Nk5x+IhA+zsGmrQtpkLiumRJEYef68gcpn2ooXhSQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", + "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", "cpu": [ "x64" ], @@ -2544,9 +2587,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.27.4.tgz", - "integrity": "sha512-NBI2/i2hT9Q+HySSHTBh52da7isru4aAAo6qC3I7QFVsuhxi2gM8t/EI9EVcILiHLj1vfi+VGGPaLOUENn7pmw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", + "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", "cpu": [ "arm64" ], @@ -2558,9 +2601,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.27.4.tgz", - "integrity": "sha512-wYcC5ycW2zvqtDYrE7deary2P2UFmSh85PUpAx+dwTCO9uw3sgzD6Gv9n5X4vLaQKsrfTSZZ7Z7uynQozPVvWA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", + "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", "cpu": [ "x64" ], @@ -2572,9 +2615,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.27.4.tgz", - "integrity": "sha512-9OwUnK/xKw6DyRlgx8UizeqRFOfi9mf5TYCw1uolDaJSbUmBxP85DE6T4ouCMoN6pXw8ZoTeZCSEfSaYo+/s1w==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", + "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", "cpu": [ "arm" ], @@ -2586,9 +2629,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.27.4.tgz", - "integrity": "sha512-Vgdo4fpuphS9V24WOV+KwkCVJ72u7idTgQaBoLRD0UxBAWTF9GWurJO9YD9yh00BzbkhpeXtm6na+MvJU7Z73A==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", + "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", "cpu": [ "arm" ], @@ -2600,9 +2643,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.27.4.tgz", - "integrity": "sha512-pleyNgyd1kkBkw2kOqlBx+0atfIIkkExOTiifoODo6qKDSpnc6WzUY5RhHdmTdIJXBdSnh6JknnYTtmQyobrVg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", + "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", "cpu": [ "arm64" ], @@ -2614,9 +2657,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.27.4.tgz", - "integrity": "sha512-caluiUXvUuVyCHr5DxL8ohaaFFzPGmgmMvwmqAITMpV/Q+tPoaHZ/PWa3t8B2WyoRcIIuu1hkaW5KkeTDNSnMA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", + "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", "cpu": [ "arm64" ], @@ -2627,10 +2670,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", + "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.27.4.tgz", - "integrity": "sha512-FScrpHrO60hARyHh7s1zHE97u0KlT/RECzCKAdmI+LEoC1eDh/RDji9JgFqyO+wPDb86Oa/sXkily1+oi4FzJQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", + "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", "cpu": [ "ppc64" ], @@ -2642,9 +2699,23 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.27.4.tgz", - "integrity": "sha512-qyyprhyGb7+RBfMPeww9FlHwKkCXdKHeGgSqmIXw9VSUtvyFZ6WZRtnxgbuz76FK7LyoN8t/eINRbPUcvXB5fw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", + "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", + "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", "cpu": [ "riscv64" ], @@ -2656,9 +2727,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.27.4.tgz", - "integrity": "sha512-PFz+y2kb6tbh7m3A7nA9++eInGcDVZUACulf/KzDtovvdTizHpZaJty7Gp0lFwSQcrnebHOqxF1MaKZd7psVRg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", + "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", "cpu": [ "s390x" ], @@ -2670,9 +2741,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.4.tgz", - "integrity": "sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", + "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", "cpu": [ "x64" ], @@ -2684,9 +2755,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.27.4.tgz", - "integrity": "sha512-5AeeAF1PB9TUzD+3cROzFTnAJAcVUGLuR8ng0E0WXGkYhp6RD6L+6szYVX+64Rs0r72019KHZS1ka1q+zU/wUw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", + "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", "cpu": [ "x64" ], @@ -2698,9 +2769,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.27.4.tgz", - "integrity": "sha512-yOpVsA4K5qVwu2CaS3hHxluWIK5HQTjNV4tWjQXluMiiiu4pJj4BN98CvxohNCpcjMeTXk/ZMJBRbgRg8HBB6A==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", + "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", "cpu": [ "arm64" ], @@ -2712,9 +2783,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.27.4.tgz", - "integrity": "sha512-KtwEJOaHAVJlxV92rNYiG9JQwQAdhBlrjNRp7P9L8Cb4Rer3in+0A+IPhJC9y68WAi9H0sX4AiG2NTsVlmqJeQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", + "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", "cpu": [ "ia32" ], @@ -2726,9 +2797,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.27.4.tgz", - "integrity": "sha512-3j4jx1TppORdTAoBJRd+/wJRGCPC0ETWkXOecJ6PPZLj6SptXkrXcNqdj0oclbKML6FkQltdz7bBA3rUSirZug==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", + "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", "cpu": [ "x64" ], @@ -2803,16 +2874,73 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "dev": true, "license": "MIT" }, @@ -2876,6 +3004,16 @@ "@types/pbf": "*" } }, + "node_modules/@types/node": { + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, "node_modules/@types/pbf": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", @@ -2943,17 +3081,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz", - "integrity": "sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz", + "integrity": "sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/type-utils": "8.15.0", - "@typescript-eslint/utils": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/type-utils": "8.16.0", + "@typescript-eslint/utils": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2977,16 +3115,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz", - "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", + "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/typescript-estree": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "debug": "^4.3.4" }, "engines": { @@ -3006,14 +3144,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz", - "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", + "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0" + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3024,14 +3162,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz", - "integrity": "sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz", + "integrity": "sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.15.0", - "@typescript-eslint/utils": "8.15.0", + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/utils": "8.16.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -3052,9 +3190,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz", - "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", + "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", "dev": true, "license": "MIT", "engines": { @@ -3066,14 +3204,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz", - "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", + "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3134,16 +3272,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz", - "integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz", + "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/typescript-estree": "8.15.0" + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3162,13 +3300,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz", - "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", + "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/types": "8.16.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -3180,15 +3318,15 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.3.tgz", - "integrity": "sha512-NooDe9GpHGqNns1i8XDERg0Vsg5SSYRhRxxyTGogUdkdNt47jal+fbuYi+Yfq6pzRCKXyoPcWisfxE6RIM3GKA==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", + "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.25.2", - "@babel/plugin-transform-react-jsx-self": "^7.24.7", - "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/core": "^7.26.0", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.2" }, @@ -3196,7 +3334,7 @@ "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "node_modules/acorn": { @@ -3342,9 +3480,9 @@ } }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -3516,9 +3654,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001684", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz", - "integrity": "sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==", + "version": "1.0.30001685", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001685.tgz", + "integrity": "sha512-e/kJN1EMyHQzgcMEEgoo+YTCO1NGCmIYHk5Qk8jT6AazWemS5QFKJ5ShCJlH3GZrNIdZofcNCEwZqbMjjKzmnA==", "dev": true, "funding": [ { @@ -3553,6 +3691,19 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz", + "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/cheap-ruler": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cheap-ruler/-/cheap-ruler-4.0.0.tgz", @@ -3560,6 +3711,21 @@ "license": "ISC", "peer": true }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3646,6 +3812,12 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/country-flag-icons": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.5.13.tgz", + "integrity": "sha512-4JwHNqaKZ19doQoNcBjsoYA+I7NqCH/mC/6f5cBWvdKzcK5TMmzLpq3Z/syVHMHJuDGFwJ+rPpGizvrqJybJow==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3682,9 +3854,129 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -3739,6 +4031,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -3756,6 +4058,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3818,6 +4126,16 @@ "node": ">=0.4.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/earcut": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.0.tgz", @@ -3842,9 +4160,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.64", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.64.tgz", - "integrity": "sha512-IXEuxU+5ClW2IGEYFC2T7szbyVgehupCWQe5GNh+H065CD6U6IFN0s4KeAMFGNmQolRU4IV7zGBWSYMmZ8uuqQ==", + "version": "1.5.67", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.67.tgz", + "integrity": "sha512-nz88NNBsD7kQSAGGJyp8hS6xSPtWwqNogA0mjtc2nUYeEf3nURK9qpV18TuBdDmEDgVWotS8Wkzf+V52dSQ/LQ==", "dev": true, "license": "ISC" }, @@ -3955,9 +4273,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, @@ -3990,15 +4308,15 @@ } }, "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "license": "MIT", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -4008,9 +4326,9 @@ } }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4018,32 +4336,34 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" } }, "node_modules/escalade": { @@ -4070,9 +4390,9 @@ } }, "node_modules/eslint": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", - "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz", + "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==", "dev": true, "license": "MIT", "dependencies": { @@ -4081,7 +4401,7 @@ "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.9.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.15.0", + "@eslint/js": "9.16.0", "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -4143,13 +4463,13 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.14.tgz", - "integrity": "sha512-aXvzCTK7ZBv1e7fahFuR3Z/fyQQSIQ711yPgYRj+Oj64tyTgO4iQIDmYXDBqvSWQ/FA4OSCsXOStlF+noU0/NA==", + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.16.tgz", + "integrity": "sha512-slterMlxAhov/DZO8NScf6mEeMBBXodFUolijDvrtTxyezyLoTQaa73FyYus/VbTdftd8wBgBxPMRk3poleXNQ==", "dev": true, "license": "MIT", "peerDependencies": { - "eslint": ">=7" + "eslint": ">=8.40" } }, "node_modules/eslint-scope": { @@ -4253,6 +4573,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4260,6 +4586,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-equals": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", + "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -4653,9 +4988,9 @@ } }, "node_modules/globals": { - "version": "15.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz", - "integrity": "sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==", + "version": "15.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.13.0.tgz", + "integrity": "sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==", "dev": true, "license": "MIT", "engines": { @@ -4683,13 +5018,16 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.1.0.tgz", + "integrity": "sha512-FQoVQnqcdk4hVM4JN1eromaun4iuS34oStkdlLENLdpULsuQcTyXj8w7ayhuUfPwEYZ1ZOooOTT6fdA9Vmx/RA==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.3" + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4750,11 +5088,14 @@ } }, "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.1.0.tgz", + "integrity": "sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, "engines": { "node": ">= 0.4" }, @@ -4894,6 +5235,27 @@ "dev": true, "license": "ISC" }, + "node_modules/input-format": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/input-format/-/input-format-0.3.11.tgz", + "integrity": "sha512-q24+iW10ZMb7KIRDlVUl3GvFcadf1ttE/QA2waINkDMdjsPXStQSOvdTyHwO8p+4Mq433ILQJZRL8YKtPjNk4g==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^18.1.0", + "react-dom": "^18.1.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/internal-slot": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", @@ -4909,6 +5271,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -4956,14 +5327,14 @@ } }, "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.0.tgz", + "integrity": "sha512-kR5g0+dXf/+kXnqI+lu0URKYPKgICtHGGNCDSB10AaUFj3o/HkB3u7WfpRBJGFopxxY0oH3ux7ZsDjLtK7xqvw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bind": "^1.0.7", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5132,13 +5503,14 @@ } }, "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.0.tgz", + "integrity": "sha512-KVSZV0Dunv9DTPkhXwcZ3Q+tUc9TsaE1ZwX5J2WMvsSGS6Md8TFPun5uwh0yRdrNerI6vf/tbJxqSx4c1ZI1Lw==", "dev": true, "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bind": "^1.0.7", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5158,14 +5530,16 @@ } }, "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.0.tgz", + "integrity": "sha512-B6ohK4ZmoftlUe+uvenXSbPJFo6U37BH7oO1B3nQH8f/7h27N56s85MhUtbFJAziz5dcmuR3i8ovUl35zp8pFA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bind": "^1.0.7", + "gopd": "^1.1.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -5227,13 +5601,14 @@ } }, "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.0.tgz", + "integrity": "sha512-PlfzajuF9vSo5wErv3MJAKD/nqf9ngAs1NFQYm16nUYFO2IzxJ2hcm+IOCg+EEopdykNNUhVq5cz35cAUxU8+g==", "dev": true, "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bind": "^1.0.7", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5508,6 +5883,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.15.tgz", + "integrity": "sha512-M7+rtYi9l5RvMmHyjyoF3BHHUpXTYdJ0PezZGHNs0GyW1lO+K7jxlXpbdIb7a56h0nqLYdjIw+E+z0ciGaJP7g==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5528,7 +5909,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, "license": "MIT" }, "node_modules/lodash.debounce": { @@ -5707,9 +6087,9 @@ "peer": true }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -5739,6 +6119,15 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", @@ -5892,9 +6281,9 @@ "license": "MIT" }, "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, @@ -5943,9 +6332,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "dev": true, "funding": [ { @@ -5963,7 +6352,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6001,6 +6390,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/protocol-buffers-schema": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", @@ -6074,6 +6474,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -6087,6 +6497,21 @@ "react": "^18.3.1" } }, + "node_modules/react-icons": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", + "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-leaflet": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", @@ -6101,6 +6526,23 @@ "react-dom": "^18.0.0" } }, + "node_modules/react-phone-number-input": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/react-phone-number-input/-/react-phone-number-input-3.4.9.tgz", + "integrity": "sha512-RG40GTjfJwBR5whpEkQMvMMKcbqQSlXiKfiTp2mYoULkTYwxFn04iAVplRizWi3yLPL0fQiL4U+YU+9MIQGZog==", + "license": "MIT", + "dependencies": { + "classnames": "^2.5.1", + "country-flag-icons": "^1.5.11", + "input-format": "^0.3.10", + "libphonenumber-js": "^1.11.12", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -6112,15 +6554,13 @@ } }, "node_modules/react-router": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.0.1.tgz", - "integrity": "sha512-WVAhv9oWCNsja5AkK6KLpXJDSJCQizOIyOd4vvB/+eHGbYx5vkhcmcmwWjQ9yqkRClogi+xjEg9fNEOd5EX/tw==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz", + "integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==", "license": "MIT", "dependencies": { - "@types/cookie": "^0.6.0", "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0", - "turbo-stream": "2.4.0" + "set-cookie-parser": "^2.6.0" }, "engines": { "node": ">=20.0.0" @@ -6136,12 +6576,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.0.1.tgz", - "integrity": "sha512-duBzwAAiIabhFPZfDjcYpJ+f08TMbPMETgq254GWne2NW1ZwRHhZLj7tpSp8KGb7JvZzlLcjGUnqLxpZQVEPng==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.0.tgz", + "integrity": "sha512-DYgm6RDEuKdopSyGOWZGtDfSm7Aofb8CCzgkliTjtu/eDuB0gcsv6qdFhhi8HdtmA+KHkt5MfZ5K2PdzjugYsA==", "license": "MIT", "dependencies": { - "react-router": "7.0.1" + "react-router": "7.6.0" }, "engines": { "node": ">=20.0.0" @@ -6151,6 +6591,75 @@ "react-dom": ">=18" } }, + "node_modules/react-smooth": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.3.tgz", + "integrity": "sha512-PyxIrra8WZWrMRFcCiJsZ+JqFaxEINAt+v/w++wQKQlmO99Eh3+JTLeKApdTsLX2roBdWYXqPsaS8sO4UmdzIg==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/recharts": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.14.1.tgz", + "integrity": "sha512-xtWulflkA+/xu4/QClBdtZYN30dbvTHjxjkh5XTMrH/CQ3WGDDPHHa/LLKCbgoqz0z3UaSH2/blV1i6VNMeh1g==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.0", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/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==", + "license": "MIT" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.7.tgz", @@ -6193,13 +6702,6 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true, - "license": "MIT" - }, "node_modules/regenerator-transform": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", @@ -6327,13 +6829,13 @@ } }, "node_modules/rollup": { - "version": "4.27.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.27.4.tgz", - "integrity": "sha512-RLKxqHEMjh/RGLsDxAEsaLO3mWgyoU6x9w6n1ikAzet4B3gI2/3yP6PWY2p9QzRTh6MfEIXB3MwsOY0Iv3vNrw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", + "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.7" }, "bin": { "rollup": "dist/bin/rollup" @@ -6343,24 +6845,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.27.4", - "@rollup/rollup-android-arm64": "4.27.4", - "@rollup/rollup-darwin-arm64": "4.27.4", - "@rollup/rollup-darwin-x64": "4.27.4", - "@rollup/rollup-freebsd-arm64": "4.27.4", - "@rollup/rollup-freebsd-x64": "4.27.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.27.4", - "@rollup/rollup-linux-arm-musleabihf": "4.27.4", - "@rollup/rollup-linux-arm64-gnu": "4.27.4", - "@rollup/rollup-linux-arm64-musl": "4.27.4", - "@rollup/rollup-linux-powerpc64le-gnu": "4.27.4", - "@rollup/rollup-linux-riscv64-gnu": "4.27.4", - "@rollup/rollup-linux-s390x-gnu": "4.27.4", - "@rollup/rollup-linux-x64-gnu": "4.27.4", - "@rollup/rollup-linux-x64-musl": "4.27.4", - "@rollup/rollup-win32-arm64-msvc": "4.27.4", - "@rollup/rollup-win32-ia32-msvc": "4.27.4", - "@rollup/rollup-win32-x64-msvc": "4.27.4", + "@rollup/rollup-android-arm-eabi": "4.40.2", + "@rollup/rollup-android-arm64": "4.40.2", + "@rollup/rollup-darwin-arm64": "4.40.2", + "@rollup/rollup-darwin-x64": "4.40.2", + "@rollup/rollup-freebsd-arm64": "4.40.2", + "@rollup/rollup-freebsd-x64": "4.40.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", + "@rollup/rollup-linux-arm-musleabihf": "4.40.2", + "@rollup/rollup-linux-arm64-gnu": "4.40.2", + "@rollup/rollup-linux-arm64-musl": "4.40.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-musl": "4.40.2", + "@rollup/rollup-linux-s390x-gnu": "4.40.2", + "@rollup/rollup-linux-x64-gnu": "4.40.2", + "@rollup/rollup-linux-x64-musl": "4.40.2", + "@rollup/rollup-win32-arm64-msvc": "4.40.2", + "@rollup/rollup-win32-ia32-msvc": "4.40.2", + "@rollup/rollup-win32-x64-msvc": "4.40.2", "fsevents": "~2.3.2" } }, @@ -6855,24 +7359,33 @@ "node": ">=10" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", - "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==", + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.2", + "fdir": "^6.4.4", "picomatch": "^4.0.2" }, "engines": { "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -6928,9 +7441,9 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.1.tgz", - "integrity": "sha512-5RU2/lxTA3YUZxju61HO2U6EoZLvBLtmV2mbTvqyu4a/7s7RmJPT+1YekhMVsQhznRWk/czIwDUg+V8Q9ZuG4w==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", "dev": true, "license": "MIT", "engines": { @@ -6940,12 +7453,6 @@ "typescript": ">=4.2.0" } }, - "node_modules/turbo-stream": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", - "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", - "license": "ISC" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7065,15 +7572,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.15.0.tgz", - "integrity": "sha512-wY4FRGl0ZI+ZU4Jo/yjdBu0lVTSML58pu6PgGtJmCufvzfV565pUF6iACQt092uFOd49iLOTX/sEVmHtbSrS+w==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.16.0.tgz", + "integrity": "sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.15.0", - "@typescript-eslint/parser": "8.15.0", - "@typescript-eslint/utils": "8.15.0" + "@typescript-eslint/eslint-plugin": "8.16.0", + "@typescript-eslint/parser": "8.16.0", + "@typescript-eslint/utils": "8.16.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7107,6 +7614,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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==", + "dev": true, + "license": "MIT" + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", @@ -7226,22 +7740,47 @@ "punycode": "^2.1.0" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { - "version": "5.4.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", - "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -7250,19 +7789,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -7283,36 +7828,60 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, "node_modules/vite-node": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.5.tgz", - "integrity": "sha512-rd0QIgx74q4S1Rd56XIiL2cYEdyWn13cunYBIuqh9mpmQr7gGS0IxXoP8R6OaZtNQQLyXSWbd4rXKYUbhFpK5w==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.3.tgz", + "integrity": "sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" + "debug": "^4.4.0", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-node/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/vite-plugin-pwa": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.21.0.tgz", - "integrity": "sha512-gnDE5sN2hdxA4vTl0pe6PCTPXqChk175jH8dZVVTBjFhWarZZoXaAdoTIKCIa8Zbx94sC0CnCOyERBWpxvry+g==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.21.1.tgz", + "integrity": "sha512-rkTbKFbd232WdiRJ9R3u+hZmf5SfQljX1b45NF6oLA6DSktEKpYllgTo1l2lkiZWMWV78pABJtFjNXfBef3/3Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7330,7 +7899,7 @@ }, "peerDependencies": { "@vite-pwa/assets-generator": "^0.2.6", - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "workbox-build": "^7.3.0", "workbox-window": "^7.3.0" }, @@ -7340,6 +7909,34 @@ } } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vt-pbf": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", @@ -7452,9 +8049,9 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.16.tgz", + "integrity": "sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/client/package.json b/client/package.json index 6d25c6e..ec86cd9 100644 --- a/client/package.json +++ b/client/package.json @@ -7,27 +7,32 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview --base /GlowPath/", - "this": "vite-node src/services/amenitiesService.tsx" + "preview": "vite preview --base /GlowPath/" }, "dependencies": { "@here/flexpolyline": "^0.1.0", "@types/leaflet-rotatedmarker": "^0.2.5", "axios": "^1.7.7", "boundingbox": "^1.4.0", + "date-fns": "^4.1.0", "leaflet": "^1.9.4", "leaflet-polylinedecorator": "^1.6.0", "leaflet-rotatedmarker": "^0.2.0", "mapbox-gl-leaflet": "^0.0.16", "react": "^18.3.1", + "react-chartjs-2": "^5.2.0", "react-dom": "^18.3.1", + "react-icons": "^5.4.0", "react-leaflet": "^4.2.1", + "react-phone-number-input": "^3.4.9", "react-router-dom": "^7.0.1", + "recharts": "^2.14.1", "socket.io-client": "^4.8.1" }, "devDependencies": { "@eslint/js": "^9.13.0", "@types/leaflet": "^1.9.14", + "@types/node": "^22.10.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/serviceworker": "^0.0.106", @@ -38,8 +43,8 @@ "globals": "^15.11.0", "typescript": "~5.6.2", "typescript-eslint": "^8.11.0", - "vite": "^5.4.10", - "vite-node": "^2.1.5", + "vite": "^6.3.5", + "vite-node": "^3.1.3", "vite-plugin-pwa": "^0.21.0" } } diff --git a/client/public/alarm.mp3 b/client/public/alarm.mp3 new file mode 100644 index 0000000..5ac5603 Binary files /dev/null and b/client/public/alarm.mp3 differ diff --git a/client/public/manifest.json b/client/public/manifest.json new file mode 100644 index 0000000..3f5f8f8 --- /dev/null +++ b/client/public/manifest.json @@ -0,0 +1,32 @@ +{ + "name": "GlowPath", + "start_url": "/", + "icons": [ + { + "src": "images/icon-72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "images/icon-128.png", + "sizes": "128x128", + "type": "image/png" + }, + { + "src": "images/icon-144.png", + "sizes": "144x144", + "type": "image/png" + }, + { + "src": "images/icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "images/icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "display": "standalone" + } \ No newline at end of file diff --git a/client/public/search.svg b/client/public/search.svg new file mode 100644 index 0000000..70ebe4e --- /dev/null +++ b/client/public/search.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index 2fb86cb..9566a7a 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -4,10 +4,14 @@ import { BrowserRouter as Router, Routes, Route} from 'react-router-dom'; import JourneyPage from './pages/journeyPage/JourneyPage'; import WhereToPage from './pages/WhereToPage'; import HomePage from './pages/HomePage/HomePage'; -import SettingsPage from './pages/SettingsPage' +import ProfilePage from './pages/profilePage/ProfilePage'; import NavigationPage from './pages/NaviagtionPage'; - import ObserverPage from './pages/observerPage/ObserverPage'; +import VisualisationsPage from './pages/VisualisationsPage'; +import ContactManagerPage from './pages/ContactManagerPage'; +import "./index.css" + +//import ChatPage from './pages/ChatPage'; @@ -22,9 +26,6 @@ const App: React.FC = () => {
{/* Global Navigation */} -
-

GlowPath!

-
{/* Routes */}
@@ -32,13 +33,18 @@ const App: React.FC = () => { } /> } /> } /> - } /> + + } /> + + + } /> } /> - + } /> } /> + {/* } /> - } /> - } /> */} + } /> + */}
diff --git a/client/src/Types/Express.ts b/client/src/Types/Express.ts new file mode 100644 index 0000000..f8d8fe9 --- /dev/null +++ b/client/src/Types/Express.ts @@ -0,0 +1,8 @@ +import { UserI } from "./User"; + +export interface AuthResponse { + token?: string; + message?: string; + updated?: UserI; + user?: UserI; +} diff --git a/client/src/Types/Route.ts b/client/src/Types/Route.ts new file mode 100644 index 0000000..b0b5cde --- /dev/null +++ b/client/src/Types/Route.ts @@ -0,0 +1,47 @@ +import { LatLngTuple } from "leaflet"; + +export interface ActionI { + + action: string; + duration: number; + length: number; + instruction: string; + offset: number; + +} + +export interface InstructionsI{ + _id: string; + actions: ActionI[]; + duration: number; + length: number; + instruction: string; + offset: number; + direction?: string; + severity?: string; +} + + +export interface SummaryI { + _id?: string; + duration?: number; + length?: number; + baseDuration?: number; + date: string; +} + + +export interface RouteI { + _id?: string; + polyline: LatLngTuple[]; + instructions: InstructionsI[]; + summary: SummaryI; +} + + +export interface RouteRequestI { + origin: string | number[], + destination: string | number[], + transportMode?: 'pedestrian' | 'publicTransport' | 'bicycle' | 'car' | null, + return?: 'polyline,summary,instructions,actions', +} \ No newline at end of file diff --git a/client/src/Types/Share.ts b/client/src/Types/Share.ts new file mode 100644 index 0000000..2ce77a4 --- /dev/null +++ b/client/src/Types/Share.ts @@ -0,0 +1,11 @@ + +import { RouteI } from "./Route"; +import { UserI } from "./User"; + +export interface ShareI { + _id?: string; + owner: UserI; + route: RouteI; + password: string; + date: string; +} \ No newline at end of file diff --git a/client/src/Types/User.ts b/client/src/Types/User.ts new file mode 100644 index 0000000..27df9e5 --- /dev/null +++ b/client/src/Types/User.ts @@ -0,0 +1,33 @@ + +import { SummaryI } from "./Route"; + +// User Interface extends Document so that its type has access to mongodb methods. +export interface SettingsI { + notifyNearby: boolean, + notifyAuthorities: boolean, + allowNotifications: boolean, + defaultSos: string, + theme: string, + +} + +export interface LoginDataI { + email: string; + password: string; +} + +export interface RegisterDataI extends LoginDataI { + firstName: string; + lastName: string; + telephone?: string; +} +export interface UserI extends RegisterDataI{ + _id: string; + places?: string[]; + tripHistory: SummaryI[], + settings: SettingsI; +} + + + + diff --git a/client/src/components/Footer.tsx b/client/src/components/Footer.tsx new file mode 100644 index 0000000..c3da760 --- /dev/null +++ b/client/src/components/Footer.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; + +const Footer: React.FC = () => { + const navigate = useNavigate(); // Initialize the useNavigate hook + + return ( +
+ + + + + + + +
+ ); +}; + +export default Footer; \ No newline at end of file diff --git a/client/src/components/MapComponent/MapComponent.tsx b/client/src/components/MapComponent/MapComponent.tsx index 0df3776..f2bb430 100644 --- a/client/src/components/MapComponent/MapComponent.tsx +++ b/client/src/components/MapComponent/MapComponent.tsx @@ -1,4 +1,3 @@ - import { useEffect, useState } from "react"; import { MapContainer, @@ -11,62 +10,69 @@ import { import L from "leaflet"; // for map manipulation import "leaflet-rotatedmarker"; // plugin for rotated markers import { latLng, LatLng, LatLngTuple } from "leaflet"; -import "leaflet/dist/leaflet.css"; -import "../../styles/MapComponent.css"; -import { Amenity, fetchAmenities } from "../../services/amenitiesService"; +import "leaflet/dist/leaflet.css"; +import "../../styles/MapComponent.css"; import mapThemes, { getDefaultTheme, isValidTheme } from "./MapThemes"; -import FitBounds from "./FitBounds"; +import FitBounds from "./FitBounds"; +import { InstructionsI, SummaryI } from "../../Types/Route"; +import SosButton from "../../pages/journeyPage/SosButton"; -// define the interface for MapComponent props -interface MapComponentProps { +export interface MapComponentProps { latitude: number | null; // User's latitude - longitude: number |null; // User's longitude + longitude: number | null; // User's longitude geolocationError: string | null; // Geolocation error message - route: LatLngTuple[]; // Route polyline coordinates - summary: { distance: number; duration: number } | null; // Route summary - instructions: any[]; // Turn-by-turn instructions + polyline: LatLngTuple[]; // Route polyline coordinates + summary: SummaryI | null;// Route summary + instructions: InstructionsI[]; // Turn-by-turn instructions originCoords: LatLng | null; // Origin coordinates destinationCoords: LatLng | null; // Destination coordinates theme: string; // Map theme - heading: number |null; // Heading for the rotated marker -} + heading: number | null; // Heading for the rotated marker + litStreets: LatLngTuple[]; // Array of lit street coordinates + sidewalks: { geometry: { lat: number; lon: number }[] }[]; // Array of sidewalk geometries + policeStations: LatLngTuple[]; // Police station locations + hospitals: LatLngTuple[]; // Hospital locations +} const MapComponent: React.FC = ({ latitude, longitude, geolocationError, - route, summary, instructions, + polyline, originCoords, destinationCoords, theme, heading, + litStreets = [], + sidewalks = [], + policeStations = [], + hospitals = [], + }) => { - const [amenities, setAmenities] = useState([]); // state for amenities const [isInsideGeofence, setIsInsideGeofence] = useState(false); // state for geofence status // validate and set the map theme const validatedTheme = isValidTheme(theme) ? theme : getDefaultTheme(); // define geofence parameters - const fenceCenter = latitude && longitude ? latLng(latitude, longitude) : null; + const fenceCenter = latitude && longitude ? latLng(latitude, longitude) : null; const fenceRadius = 100; // Geofence radius in meters - // Icons for turn-by-turn instructions + const actionIcons: Record = { - depart: "🏁", - arrive: "🏁", - left: "⬅️", + depart: "🏁", + arrive: "🏁", + left: "⬅️", straight: "⬆️", }; - // Monitor user's position and check if inside the geofence useEffect(() => { if (originCoords && fenceCenter) { - const distance = fenceCenter.distanceTo(originCoords); // Calculate distance from origin - const isInside = distance <= fenceRadius; // Check if inside geofence + const distance = fenceCenter.distanceTo(originCoords); + const isInside = distance <= fenceRadius; setIsInsideGeofence(isInside); console.log( @@ -79,30 +85,12 @@ const MapComponent: React.FC = ({ }, [originCoords, fenceCenter, fenceRadius]); // fetch nearby amenities - useEffect(() => { - const fetchData = async () => { - if (!originCoords) return; - try { - const data = await fetchAmenities(500, { - lat: originCoords.lat, - lon: originCoords.lng, - }); // Pass origin coordinates - if (data && Array.isArray(data)) { - setAmenities(data); // Update amenities state - } else { - throw new Error("Invalid response received or no amenities found."); - } - } catch (error) { - console.error("Error fetching amenities:", error); - } - }; - - fetchData(); - }, [originCoords]); return (
+ {/* Map Container */} + = ({ scrollWheelZoom={false} style={{ height: "80vh", width: "100%" }} > + {/* Tile Layer for Map Theme */} {mapThemes[validatedTheme] ? ( = ({

Invalid map theme URL

)} + + + + {/* Lit Streets */} + {litStreets.map(([lat, lon], index) => ( + + ))} + + {/* Sidewalks */} + {sidewalks.map((sidewalk, index) => ( + latLng(lat, lon))} + color="green" + /> + ))} + + {/* Police Stations */} + {policeStations.map(([lat, lon], index) => ( + + Police Station + + ))} + + {/* Hospitals */} + {hospitals.map(([lat, lon], index) => ( + + Hospital + + ))} + {/* Fit map bounds to origin and destination */} {originCoords && destinationCoords && ( @@ -130,11 +166,11 @@ const MapComponent: React.FC = ({ Current Position @@ -146,41 +182,23 @@ const MapComponent: React.FC = ({ Destination
- Coordinates: {destinationCoords.lat.toFixed(4)},{" "} - {destinationCoords.lng.toFixed(4)} + Coordinates: {destinationCoords.lat.toFixed(4)}, {destinationCoords.lng.toFixed(4)}
)} {/* Route Polyline */} - {route.length > 0 && ( - + {polyline.length > 0 && ( + )} - {/* Amenities Markers */} - {amenities.map((amenity: Amenity, index: number) => { - const { lat, lon, tags } = amenity; - const name = tags.name || "Unnamed Amenity"; - return ( - - - {name}
- {tags.public_transport || ""}
- {tags.lit ? "Lit space" : ""} -
-
- ); - })} {/* Geofence Circle */} {fenceCenter && ( @@ -210,14 +228,13 @@ const MapComponent: React.FC = ({ {/* Route Summary */}

Route Summary

- {summary ? ( + {summary && summary.length && summary.duration? (

- Distance: {summary.distance / 1000} km + Distance: {summary.length / 1000} km

- Duration: {Math.ceil(summary.duration / 60)}{" "} - minutes + Duration: {Math.ceil(summary.duration / 60)} minutes

) : ( @@ -230,20 +247,20 @@ const MapComponent: React.FC = ({

Turn-by-Turn Instructions

    {instructions.map((instruction, index) => { - const { instruction: text, action, length, duration } = instruction; - const icon = actionIcons[action] || "➑️"; // generic arrow + const { instruction: text, actions, length, duration } = instruction; + const actionIconssString = actions + .map((action) => actionIcons[action.instruction] || "➑️"); return (
  • - {icon} {action}: + {actionIconssString} {text}: {" "} {text}
    Distance: {length || "Unknown"} m
    - Duration:{" "} - {duration ? `${Math.ceil(duration / 60)} min` : "Unknown"} + Duration: {duration ? `${Math.ceil(duration / 60)} min` : "Unknown"}
  • ); @@ -254,5 +271,4 @@ const MapComponent: React.FC = ({ ); }; - export default MapComponent; \ No newline at end of file diff --git a/client/src/components/MapComponent/MapThemes.tsx b/client/src/components/MapComponent/MapThemes.tsx index 514470b..b6ea951 100644 --- a/client/src/components/MapComponent/MapThemes.tsx +++ b/client/src/components/MapComponent/MapThemes.tsx @@ -1,14 +1,14 @@ -// Map themes with corresponding tile URLs const mapThemes: { [key: string]: string } = { - standard: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", - dark: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", - satellite: - "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", - }; + dark: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + lighttime: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", + satellite: + "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", - export const getDefaultTheme = (): string => "standard"; - - export const isValidTheme = (themeKey: string): boolean => - Object.keys(mapThemes).includes(themeKey); - - export default mapThemes; \ No newline at end of file +}; + +export const getDefaultTheme = (): string => "dark"; + +export const isValidTheme = (themeKey: string): boolean => + Object.keys(mapThemes).includes(themeKey); + +export default mapThemes; \ No newline at end of file diff --git a/client/src/contexts/UserContext.tsx b/client/src/contexts/UserContext.tsx new file mode 100644 index 0000000..a8f0491 --- /dev/null +++ b/client/src/contexts/UserContext.tsx @@ -0,0 +1,78 @@ +import React, { createContext, useEffect, useState } from 'react'; +import { RegisterDataI, UserI } from '../Types/User'; +import { registerService, fetchUserProfile } from "../services/authService"; // you'll need to implement this + +interface AppContextValue { + user: UserI | null; + setUser: React.Dispatch>; + isAuthorized: boolean; + handleLoginContext: (token: string, userResponse: UserI) => void; + handleLogoutContext: () => void; + handleRegisterContext: (userResponse: UserI) => void; +} + +export const AuthContext = createContext({ + user: null, + isAuthorized: false, + handleLoginContext: () => { }, + handleLogoutContext: () => { }, + handleRegisterContext: () => { }, + setUser: () => { }, +}); + +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [isAuthorized, setIsAuthorized] = useState(false); + const [user, setUser] = useState(null); + + const handleLoginContext = (token: string, user: UserI) => { + setIsAuthorized(true); + localStorage.setItem("token", token); + setUser(user); + }; + + const handleRegisterContext = async (userData: RegisterDataI) => { + try { + const response = await registerService(userData); + if (response?.data.token && response.data.updated) { + setIsAuthorized(true); + localStorage.setItem("token", response.data.token); + setUser(response.data.updated); + } + } catch (error) { + console.log(error); + } + }; + + const handleLogoutContext = () => { + localStorage.removeItem("token"); + setIsAuthorized(false); + setUser(null); + }; + + useEffect(() => { + const token = localStorage.getItem('token'); + if (token) { + setIsAuthorized(true); + (async () => { + try { + const fetchedUser = await fetchUserProfile(); + setUser(fetchedUser); + } catch (err) { + console.error("Failed to fetch user profile:", err); + setIsAuthorized(false); + setUser(null); + } + })(); + } else { + setIsAuthorized(false); + setUser(null); + } + }, []); + + + return ( + + {children} + + ); +}; diff --git a/client/src/hooks/usePosition.tsx b/client/src/hooks/usePosition.tsx index 3aecfdb..199fac0 100644 --- a/client/src/hooks/usePosition.tsx +++ b/client/src/hooks/usePosition.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; export interface PositionI { timestamp: number | null; @@ -18,11 +18,12 @@ const initialPosition = { heading: null, speed: null, error: "" -} +}; export const usePosition = () => { const [position, setPosition] = useState(initialPosition); const [error, setError] = useState(""); + const watchIdRef = useRef(null); // Use a ref to store the watcher ID function handleSuccess (position: GeolocationPosition) { setPosition({ @@ -31,7 +32,7 @@ export const usePosition = () => { longitude: position.coords.longitude, accuracy: position.coords.accuracy, heading: position.coords.heading, - speed: position.coords.speed + speed: position.coords.speed, }); } @@ -44,13 +45,20 @@ export const usePosition = () => { const options = { enableHighAccuracy: true, maximumAge: 30000, - timeout: 60000 - } - navigator.geolocation.watchPosition(handleSuccess, handleError, options); + timeout: 600000, + }; + console.log("Initializing watcher on geolocation"); + watchIdRef.current = navigator.geolocation.watchPosition(handleSuccess, handleError, options); } else { setError("Navigator doesn't support geolocation"); } - }, []) - - return {...position, error} as PositionI; -} \ No newline at end of file + return () => { + console.log("Clearing watcher on geolocation"); + if (watchIdRef.current !== null) { + navigator.geolocation.clearWatch(watchIdRef.current); + } + }; + }, []); // Dependencies remain empty since `watchIdRef` is a ref + console.log('my position', position.latitude) + return { ...position, error } as PositionI; +}; diff --git a/client/src/hooks/useSocket.tsx b/client/src/hooks/useSocket.tsx index c7e01d9..44621de 100644 --- a/client/src/hooks/useSocket.tsx +++ b/client/src/hooks/useSocket.tsx @@ -1,9 +1,11 @@ -import { io } from "socket.io-client"; -import { useState, useEffect } from 'react'; +import { io, Socket } from "socket.io-client"; +import { useState, useEffect, useRef } from 'react'; import { getToken } from "../utilities/token"; import { PositionI } from "./usePosition"; -const socketServer = import.meta.env.BACKEND_URL; - +const socketServer = import.meta.env.VITE_BACKEND_URL +if (!socketServer) { + console.error('Error loading socket server from backend') +} interface MessageI { text: string; } @@ -11,81 +13,119 @@ interface MessageI { interface AlarmI { text: string; } +let socket: Socket | null; -export const useSocket = ({ password }: {password?: string}) => { +export const useSocket = ({ password }: { password?: string }) => { const [isConnected, setIsConnected] = useState(false); const [position, setPosition] = useState(); const [messages, setMessages] = useState([]); const [alarms, setAlarms] = useState([]); const [error, setError] = useState(""); - const socket = io(socketServer, { - autoConnect: false, - auth: cb => cb( - () => { - if (password) { - return {password}; - } else { - return {token: getToken()} - } - } - ) - }); - - function connectSocket () { - if (!isConnected) socket.connect(); + const initialized = useRef(false); + if (!socket) { + socket = io(socketServer, { + auth: (cb) => { + cb(password ? { password } : { token: getToken() }); // Pass credentials to the socket server + }, + autoConnect: false, // Prevent auto-connect on initialization + }); } + const connectSocket = () => { + if (!isConnected) { + socket?.connect(); + } else console.log('am i connected in hook?', isConnected) + }; + function hostShare (id: string) { - if (isConnected) socket.emit("host-share", id); + if (isConnected && socket) socket.emit("host-share", id); } function joinShare (id: string) { - if (isConnected) socket.emit("join-share", id); + console.log("isConnected: ", isConnected); + if (isConnected && socket) { + socket.emit("join-share", id, (response: string) => { + console.log('here is the response: ', response); + }); + } } function sendPosition (position: PositionI) { - if (isConnected) socket.emit("position", position); + if (!position) { + console.warn("No position to send!"); + return; + } + + if (isConnected && socket) { + console.log("Sending position to the server: ", position); + socket.emit("position", position); + } } function sendMessage (message: MessageI) { - if (isConnected) socket.emit("message", message); + if (isConnected && socket) socket.emit("message", message); } function sendAlarm (alarm: AlarmI) { - if (isConnected) socket.emit("alarm", alarm); + if (isConnected && socket) socket.emit("alarm", alarm); } useEffect(() => { + if (initialized.current) return; + initialized.current = true; + + - socket.on("connect", () => { - console.log("Socket connected"); + + socket?.on("connect", () => { + console.log("Socket connected in useSocket"); setIsConnected(true); }); - socket.on("connect_error", (error) => { - console.log("Socket error: " + error); + socket?.on("connect_error", (error) => { + console.error("Socket connection error:", error.message); setError(error.message); setIsConnected(false); - }) + }); - socket.on("location", (newPosition) => { - setPosition(newPosition); + socket?.on("disconnect", () => { + console.log("Socket disconnected"); + setIsConnected(false); }); - socket.on("message", (newMessage) => { - setMessages(previous => [...previous, newMessage]); + + socket?.on("position", (newPosition) => { + console.log("Client: Received position update from server:", newPosition); + if (!newPosition) { + console.error("Client: Received empty position data."); + return; + } + setPosition(newPosition); }); - socket.on("alarm", (newAlarm) => { - setAlarms(previous => [...previous, newAlarm]); - }) + socket?.on("message", (newMessage: MessageI) => { + console.log('message received!') + setMessages((previous) => [...previous, newMessage]); + }); - return () => { - socket.disconnect(); - } + socket?.on("alarm", (newAlarm: AlarmI) => { + console.log('alarm received') + setAlarms((previous) => [...previous, newAlarm]); + }); - },[]); - return { position, messages, alarms, error, sendPosition, sendMessage, sendAlarm, hostShare, joinShare, connectSocket }; + return () => { + console.log("Socket will disconnect because component gets unmounted"); + socket?.off("connect"); + socket?.off("connect_error"); + socket?.off("disconnect"); + socket?.off("message"); + socket?.off("alarm"); + socket?.disconnect(); + }; + // by no means add useConnect to dev dependencies! it messes everything up. + }, []); + + return { isConnected, position, messages, alarms, error, sendPosition, sendMessage, sendAlarm, hostShare, joinShare, connectSocket }; } \ No newline at end of file diff --git a/client/src/hooks/useUser.tsx b/client/src/hooks/useUser.tsx new file mode 100644 index 0000000..91c64b7 --- /dev/null +++ b/client/src/hooks/useUser.tsx @@ -0,0 +1,32 @@ +import { useContext } from "react"; +import { AuthContext } from "../contexts/UserContext"; +import { UserI } from "../Types/User"; +import { editProfileService } from "../services/authService"; + + +export const useUser = () => { + const { user, setUser } = useContext(AuthContext); + const updateUser = async (updatedFields: Partial) => { + if (!user) return; + + try { + const payload = { + _id: user._id, + ...updatedFields, + }; + + await editProfileService(payload, setUser); + console.log(`[hook, updateUser]: firstname is ${user.firstName}`) + + } catch (err) { + console.error("Error updating user profile:", err); + } + }; + + + + return { + user, + updateUser + }; +}; diff --git a/client/src/index.css b/client/src/index.css index e69de29..e42dc66 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -0,0 +1,52 @@ +@import url("https://fonts.googleapis.com/css2?family=Open+Sans:wght@200;300;400;500;600;700&display=swap"); + +*{ + font-family: "Open Sans", sans-serif; + +} + + + html { + box-sizing: border-box; + overflow-y: auto; + } + *, *:before, *:after { + box-sizing: inherit; + margin: 0; + padding: 0; + } + + body { + + overflow-y: auto; + } + + /* Footer Styles */ +.footer-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(to bottom, #4A464D, #2F2E30); + display: flex; + justify-content: space-around; + align-items: center; + padding: 1rem 0; + box-shadow: 0 -1px 4px rgba(0, 0, 0, 0.2); + } + + .footer-icon { + font-size: 1.5rem; + background: none; + border: none; + color: white; + cursor: pointer; + } + + .footer-icon:hover { + color: #ff6600; /* Optional hover effect */ + } + + + + \ No newline at end of file diff --git a/client/src/main.tsx b/client/src/main.tsx index 3d4bdea..0824828 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,10 +1,13 @@ -import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' +import { AuthProvider } from './contexts/UserContext.tsx' createRoot(document.getElementById('root')!).render( - + // + - , + + + //, ) \ No newline at end of file diff --git a/client/src/pages/ChatPage.tsx b/client/src/pages/ChatPage.tsx new file mode 100644 index 0000000..e69de29 diff --git a/client/src/pages/ContactManagerPage.tsx b/client/src/pages/ContactManagerPage.tsx new file mode 100644 index 0000000..aae9dda --- /dev/null +++ b/client/src/pages/ContactManagerPage.tsx @@ -0,0 +1,67 @@ +import React, { useState, useEffect } from "react"; +import "../styles/ContactManagerPage.css" + +const ContactManagerPage: React.FC = () => { + const [contacts, setContacts] = useState([]); + const [newContact, setNewContact] = useState(""); + + // Load contacts from localStorage + useEffect(() => { + const savedSettings = JSON.parse(localStorage.getItem("settings") || "{}"); + setContacts(savedSettings.emergencyContacts || []); + }, []); + + // Save contacts to localStorage + const saveContacts = (updatedContacts: string[]) => { + setContacts(updatedContacts); + const savedSettings = JSON.parse(localStorage.getItem("settings") || "{}"); + const updatedSettings = { ...savedSettings, emergencyContacts: updatedContacts }; + localStorage.setItem("settings", JSON.stringify(updatedSettings)); + }; + + // Add a new contact + const addContact = () => { + if (newContact && !contacts.includes(newContact)) { + saveContacts([...contacts, newContact]); + setNewContact(""); // Clear input field + } else if (!newContact) { + alert("Contact cannot be empty!"); + } else { + alert("Contact already exists!"); + } + }; + + // Remove a contact + const removeContact = (index: number) => { + const updatedContacts = contacts.filter((_, idx) => idx !== index); + saveContacts(updatedContacts); + }; + + return ( +
    +

    Manage Emergency Contacts

    +
    + setNewContact(e.target.value)} + /> + +
    +
      + {contacts.map((contact, idx) => ( +
    • + {contact} + +
    • + ))} +
    + +
    + ); +}; + +export default ContactManagerPage; \ No newline at end of file diff --git a/client/src/pages/HomePage/HomeMap.tsx b/client/src/pages/HomePage/HomeMap.tsx index 565195c..4c256f2 100644 --- a/client/src/pages/HomePage/HomeMap.tsx +++ b/client/src/pages/HomePage/HomeMap.tsx @@ -4,7 +4,8 @@ import { LatLngExpression } from 'leaflet'; import 'leaflet/dist/leaflet.css'; import '../../styles/HomeMap.css'; // Add custom styles if needed import { usePosition } from '../../hooks/usePosition'; // Custom hook for live location - +import L from "leaflet"; +import SosButton from "../journeyPage/SosButton" const HomeMap: React.FC = () => { const { latitude, longitude, error: geolocationError } = usePosition(); // Get user's live location const [currentPosition, setCurrentPosition] = useState(null); @@ -23,13 +24,19 @@ const HomeMap: React.FC = () => { center={currentPosition} zoom={15} scrollWheelZoom={false} - style={{ height: '80vh', width: '100%' }} + style={{ height: '85vh', width: '100%' }} > + - + You are here! diff --git a/client/src/pages/HomePage/HomePage.tsx b/client/src/pages/HomePage/HomePage.tsx index cd9193f..c8d4763 100644 --- a/client/src/pages/HomePage/HomePage.tsx +++ b/client/src/pages/HomePage/HomePage.tsx @@ -1,15 +1,15 @@ -import React, { useState } from "react"; -import HomeMap from "../HomePage/HomeMap"; // Import HomeMap for map display -import { useNavigate } from "react-router-dom"; // Import useNavigate for navigation -import "../../styles/HomePage.css"; // Import custom styles -import WhereToPage from "../WhereToPage"; // Import WhereToPage component - +import HomeMap from "../HomePage/HomeMap"; +import "../../styles/HomePage.css"; // Custom styles +import Footer from "../../components/Footer" +import { useState } from "react"; +import "../../styles/HomePage.css"; // Custom styles +import WhereToPage from "../WhereToPage"; // WhereToPage component const HomePage: React.FC = () => { - const [isSearchExpanded, setIsSearchExpanded] = useState(false); // State to toggle search drawer - const navigate = useNavigate(); + const [isSearchExpanded, setIsSearchExpanded] = useState(false); // Toggle state for the search drawer + const handleSearchClick = () => { - setIsSearchExpanded(true); // Expand the search drawer when clicked + setIsSearchExpanded(true); // Expand the search drawer }; const handleCloseSearch = () => { @@ -17,48 +17,48 @@ const HomePage: React.FC = () => { }; return ( -
    +
    {/* Map Section */}
    +
    - {/* Search Bar Section (under the map) */} + {/* Search Bar Section */} {!isSearchExpanded && ( -
    +
    - - + Search Icon + + Search Icon
    )} {/* Expandable Search Drawer */} {isSearchExpanded && ( -
    - {/* WhereToPage Content */} +
    )} - {/* Footer Section */} -
    - {/* Navigation Icons */} - - - -
    +
    ); }; diff --git a/client/src/pages/NaviagtionPage.tsx b/client/src/pages/NaviagtionPage.tsx index 7ce9ba0..cc46062 100644 --- a/client/src/pages/NaviagtionPage.tsx +++ b/client/src/pages/NaviagtionPage.tsx @@ -2,22 +2,48 @@ import React, { useEffect, useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { MapContainer, TileLayer, Marker, Polyline, useMap } from "react-leaflet"; import "../styles/NavigationPage.css"; +import Footer from "../components/Footer" +import { useUser } from "../hooks/useUser"; +import { format } from "date-fns"; +import { SummaryI } from "../Types/Route"; + const NavigationPage: React.FC = () => { + const location = useLocation(); const navigate = useNavigate(); - + // adding user + const { user, updateUser } = useUser(); // Destructure state from the previous navigation or default values const { - route = [], // Array of coordinates for the route - summary = { distance: 0, duration: 0 }, // Distance and duration of the route - instructions = [], // Turn-by-turn instructions - theme = "standard", // Selected map theme - transportMode = "pedestrian", // Transport mode (e.g., pedestrian) + route, // Array of coordinates for the route + summary, // Distance and duration of the route + instructions, // Turn-by-turn instructions + theme, // Selected map theme + transportMode, // Transport mode (e.g., pedestrian) } = location.state || {}; - const [currentInstruction, setCurrentInstruction] = useState(""); // Current turn-by-turn instruction + useEffect(() => { + if (user && summary && summary.length && summary.duration) { + const updatedTrip: SummaryI = { + ...summary, + date: format(new Date(), "yyyy_MM_dd"), + + }; + + const updatedTripHistory = [...(user.tripHistory), updatedTrip]; + + updateUser({ + _id: user._id, + tripHistory: updatedTripHistory, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const [currentInstruction, setCurrentInstruction] = useState(""); // Current turn-by-turn instruction + console.log(currentInstruction) // Set the initial instruction when instructions change useEffect(() => { if (instructions.length > 0) { @@ -25,28 +51,26 @@ const NavigationPage: React.FC = () => { } }, [instructions]); - // component to automatically follow and fit the map bounds to the route (needs testing) + // Component to automatically follow and fit the map bounds to the route const AutoFollowMap = () => { const map = useMap(); - // Center the map on the route useEffect(() => { if (route.length > 0) { - map.fitBounds(route); + map.fitBounds(route); } - }, [map, route]); + }, [map]); return null; }; // Define tile layer URLs for different themes const tileLayerThemes: Record = { - standard: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", - dark: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", - satellite: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", + standard: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + dark: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", + satellite: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", }; - // Handle the "Start Journey" button click const handleStartJourney = () => { navigate("/journey", { @@ -63,42 +87,62 @@ const NavigationPage: React.FC = () => { return (
    {route.length > 0 ? ( - + {/* Add the selected tile layer theme */} {/* Draw the route on the map */} - + {/* Marker for the start point */} - + + {/* Marker for the end point */} {/* Automatically follow the route */} ) : ( -

    Loading map...

    +

    Loading map...

    // Display a fallback message while the map is loading )} -
    - {/* Display the current instruction */} -

    {currentInstruction}

    - {/* Display the next instruction or a default message */} -

    Towards: {instructions?.[1]?.instruction || "Destination"}

    - {/* Display route distance and duration */} -

    - {summary.distance / 1000} km β€’ {Math.ceil(summary.duration / 60)} min -

    - - - - -
    + +
    +
    +
    + πŸ“ + Current Location +
    +
    + 🏁 + {location.state?.destinationName || "Destination"} +
    +
    +

    {summary.length / 1000} km β€’ {Math.ceil(summary.duration / 60)} min

    +
    + + +
    +
    +
    ); }; diff --git a/client/src/pages/SettingsPage.tsx b/client/src/pages/SettingsPage.tsx deleted file mode 100644 index 5402fda..0000000 --- a/client/src/pages/SettingsPage.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import React, { useState } from 'react'; -import '../styles/SettingsPage.css'; - -const SettingsPage: React.FC = () => { - // State for SOS settings - const [sosSettings, setSosSettings] = useState({ - notifyContacts: true, - notifyNearbyUsers: true, - callAuthorities: true, - shareMedia: false, // Default to off - }); - - // State for theme (light or dark) - const [theme, setTheme] = useState<'light' | 'dark'>('light'); - - // Apply theme dynamically by toggling a CSS class - const toggleTheme = () => { - const newTheme = theme === 'light' ? 'dark' : 'light'; - setTheme(newTheme); - document.documentElement.setAttribute('data-theme', newTheme); // Update the theme attribute in the - }; - - // Handle SOS button click - const handleSosClick = () => { - if (sosSettings.notifyContacts) { - alert('Sending SOS to emergency contacts...'); - // Implement API or SMS logic here - } - if (sosSettings.notifyNearbyUsers) { - alert('Notifying nearby SafeWalk users...'); - // Implement real-time notification logic here - } - if (sosSettings.callAuthorities) { - alert('Calling local authorities...'); - window.open('tel:112'); // Example emergency number - } - if (sosSettings.shareMedia) { - alert('Sharing real-time audio or video...'); - // Implement media sharing logic here - } - }; - - // Handle toggling of SOS options - const handleToggle = (option: keyof typeof sosSettings) => { - setSosSettings((prev) => ({ ...prev, [option]: !prev[option] })); - }; - - return ( -
    -

    Settings

    - - {/* SOS Options */} -
    -

    SOS Button Options

    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - - {/* Appearance Section */} -
    -

    Appearance

    - -
    - -
    -

    Alerts

    -

    - mange the app alerts -

    -
    - - {/* Test SOS Button */} -
    -

    Test SOS Button

    - -
    -
    - ); -}; - -export default SettingsPage; \ No newline at end of file diff --git a/client/src/pages/VisualisationsPage.tsx b/client/src/pages/VisualisationsPage.tsx new file mode 100644 index 0000000..7559ee8 --- /dev/null +++ b/client/src/pages/VisualisationsPage.tsx @@ -0,0 +1,211 @@ +import React, { useEffect, useState } from "react"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + BarElement, + Title, + Tooltip, + Legend, +} from "chart.js"; +import { Line, Bar } from "react-chartjs-2"; +import "../styles/ Visualisations.css"; +import Footer from "../components/Footer"; +import { useUser } from "../hooks/useUser"; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + BarElement, + Title, + Tooltip, + Legend +); + +const mockData = { + myPlaces: [ + "123 Main St", + "456 Office Rd", + "789 Fitness Ave", + ], + averageRouteLength: { + timestamps: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], + lengths: [2.5, 2.8, 3.0, 3.2, 3.5, 3.7], + }, + averageTripDuration: { + timestamps: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], + durations: [30, 32, 28, 35, 33, 31], + }, + lastTrips: [ + { startTime: "2025-05-20 08:00", finishTime: "2025-05-20 08:45", duration: 45 }, + { startTime: "2025-05-19 18:15", finishTime: "2025-05-19 18:50", duration: 35 }, + { startTime: "2025-05-18 07:30", finishTime: "2025-05-18 08:10", duration: 40 }, + ], + lastShares: [ + { date: "2025-05-21", shareDuration: 10, views: 25 }, + { date: "2025-05-20", shareDuration: 5, views: 10 }, + { date: "2025-05-19", shareDuration: 15, views: 30 }, + ], + sosActivations: { + timestamps: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], + counts: [1, 3, 0, 2, 4, 1], + }, +}; + +const Visualisations: React.FC = () => { + const { user } = useUser(); + const [myPlaces, setMyPlaces] = useState(mockData.myPlaces) + const commonChartOptions = { + maintainAspectRatio: false, + responsive: true, + plugins: { + legend: { + display: true, + }, + }, + }; + + useEffect(()=>{ + if (!user || !user.places) { + console.warn('no user...using mock data') + setMyPlaces(mockData.myPlaces) + } + else { + console.log(user.places.length) + setMyPlaces(user.places) + } + }, [user]) + return ( +
    +

    Visualisations

    + + {/* My Places Section */} +
    +

    My Places

    +
      + {myPlaces.map((place: string, idx) => ( +
    • + {place} +
    • + ))} +
    +
    + + {/* Average Route Length Over Time */} +
    +

    Average Route Length Over Time

    +
    + +
    +
    + + {/* Average Trip Duration Over Time */} +
    +

    Average Trip Duration Over Time

    +
    + +
    +
    + + {/* Last Trips */} +
    +

    Last Trips

    + + + + + + + + + + {mockData.lastTrips.map((trip, idx) => ( + + + + + + ))} + +
    Start TimeFinish TimeDuration (mins)
    {trip.startTime}{trip.finishTime}{trip.duration}
    +
    + + {/* Last Shares */} +
    +

    Last Shares

    + + + + + + + + + + {mockData.lastShares.map((share, idx) => ( + + + + + + ))} + +
    DateShare Duration (mins)# of Views
    {share.date}{share.shareDuration}{share.views}
    +
    + + {/* SoS Activations Over Time */} +
    +

    SoS Activations Over Time

    +
    + +
    +
    + +
    +
    + ); +}; + +export default Visualisations; diff --git a/client/src/pages/WhereToPage.tsx b/client/src/pages/WhereToPage.tsx index c483d2e..1035535 100644 --- a/client/src/pages/WhereToPage.tsx +++ b/client/src/pages/WhereToPage.tsx @@ -1,147 +1,288 @@ -import React, { useState, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; -import { geocodeAddress } from "../services/geocodingService"; -import { fetchRoute } from "../services/RoutingService"; -import { usePosition } from "../hooks/usePosition"; -import { LatLngTuple, latLng } from "leaflet"; -import { decode } from "@here/flexpolyline"; -import MapComponent from "../components/MapComponent/MapComponent"; - -const WhereToPage: React.FC = () => { - const navigate = useNavigate(); - const { latitude, longitude, error: geoError } = usePosition(); - const [originCoords, setOriginCoords] = useState<{ lat: number; lon: number } | null>(null); - const [destination, setDestination] = useState(""); - const [route, setRoute] = useState([]); - const [summary, setSummary] = useState<{ distance: number; duration: number } | null>(null); - const [instructions, setInstructions] = useState([]); - const [transportMode, setTransportMode] = useState<"pedestrian" | "publicTransport" | "bicycle" | "car">("pedestrian"); - const [mapTheme, setMapTheme] = useState("standard"); - const [error, setError] = useState(null); - - // Simulated heading for testing rotated markers - const heading = 0; // Default heading, you can update this dynamically if available - - // Update origin coordinates when geolocation updates - useEffect(() => { - if (latitude && longitude) { - setOriginCoords({ lat: latitude, lon: longitude }); - } - }, [latitude, longitude]); + import { useState, useEffect } from "react"; + import { useNavigate } from "react-router-dom"; + import { geocodeAddress } from "../services/geocodingService"; + import { fetchRoute } from "../services/RoutingService"; + import { usePosition } from "../hooks/usePosition"; + import { LatLngTuple, latLng } from "leaflet"; + import { decode } from "@here/flexpolyline"; + import MapComponent from "../components/MapComponent/MapComponent"; + import { InstructionsI, RouteRequestI, SummaryI } from "../Types/Route"; + import "../styles/WhereToPage.css" + import { format } from "date-fns"; + import { FaHeart, FaRegHeart } from "react-icons/fa"; + import { useUser } from "../hooks/useUser"; + + + + + const WhereToPage: React.FC = () => { + + const navigate = useNavigate(); + const { latitude, longitude, error: geoError } = usePosition(); + const [originCoords, setOriginCoords] = useState<{ lat: number; lon: number } | null>(null); + const [destination, setDestination] = useState(""); + const [searchHistory, setSearchHistory] = useState([]); // Added state for search history + const [route, setRoute] = useState([]); + const [summary, setSummary] = useState(null); + const [instructions, setInstructions] = useState([]); + const [transportMode, setTransportMode] = useState<"pedestrian" | "publicTransport" | "bicycle" | "car">("pedestrian"); + const [error, setError] = useState(null); + const [favoritePlaces, setFavoritePlaces] = useState([]); + const [activeTab, setActiveTab] = useState<"history" | "favorites">("history"); + + const { user, updateUser } = useUser(); + + + - const handleSearch = async () => { - if (!originCoords) { - setError("Unable to fetch your current location. Please try again."); - return; + const heading = 0; // Default heading + + useEffect(() => { + if (latitude && longitude) { + setOriginCoords({ lat: latitude, lon: longitude }); + } + + // Load search history from localStorage + const savedHistory = JSON.parse(localStorage.getItem("searchHistory") || "[]"); + setSearchHistory(savedHistory); + }, [latitude, longitude]); + + + useEffect(() => { + if (user && user.places) { + setFavoritePlaces(user.places) + } + }, [user]); + + const toggleFavorite = (place: string) => { + let updatedFavorites; + if (favoritePlaces.includes(place)) { + updatedFavorites = favoritePlaces.filter((item) => item !== place); + } else { + updatedFavorites = [...favoritePlaces, place]; + + } + setFavoritePlaces(updatedFavorites); + localStorage.setItem("favoritePlaces", JSON.stringify(updatedFavorites)); + submitFavorite(updatedFavorites) + }; + + const submitFavorite = async(newPlaces: string[]) => { + if (!user) { + console.warn(`[submitFavorite]: no user found`) + return; + } + try { + await updateUser({places: newPlaces}) + } + catch (e) { + console.error(`[submitFavorite]: Server error: ${e}`) + } } - try { - const destinationCoords = await geocodeAddress(destination); - const routeResponse = await fetchRoute( - [originCoords.lat, originCoords.lon], - [destinationCoords.lat, destinationCoords.lon], - transportMode - ); - - const { polyline, instructions: routeInstructions, summary: routeSummary } = routeResponse; - - const decoded = decode(polyline); - const routeCoordinates: LatLngTuple[] = decoded.polyline.map(([lat, lon]) => [lat, lon]); - - setRoute(routeCoordinates); - setSummary({ distance: routeSummary.length, duration: routeSummary.duration }); - setInstructions(routeInstructions); - - navigate("/navigation", { - state: { - originCoords: latLng(originCoords.lat, originCoords.lon), - destinationCoords: latLng(destinationCoords.lat, destinationCoords.lon), - route: routeCoordinates, - summary: { distance: routeSummary.length, duration: routeSummary.duration }, + const handleSearch = async () => { + if (!originCoords) { + setError("Unable to fetch your current location. Please try again."); + return; + } + + try { + const destinationCoords = await geocodeAddress(destination); + const routeRequest: RouteRequestI = { + origin: [originCoords.lat, originCoords.lon], + destination: [destinationCoords.lat, destinationCoords.lon], + transportMode: transportMode, + } + const routeResponse = await fetchRoute( + routeRequest + ); + + + + const { + polyline, instructions: routeInstructions, - theme: mapTheme, - }, - }); - } catch (err) { - console.error("Error during geocoding or fetching route:", err); - setError("Failed to find the location or route. Please try again."); - } - }; + summary: routeSummary, + litStreets, + sidewalks, + policeStations, + hospitals, + } = routeResponse; - return ( -
    -

    Where to?

    - - {/* Current Location */} -
    -

    - Your Current Location:{" "} - {originCoords ? `${originCoords.lat.toFixed(4)}, ${originCoords.lon.toFixed(4)}` : "Fetching your location..."} -

    - {geoError &&

    Geolocation Error: {geoError}

    } -
    + const decoded = decode(polyline); + const routeCoordinates: LatLngTuple[] = decoded.polyline.map(([lat, lon]) => [lat, lon]); - {/* Destination Input */} -
    - - setDestination(e.target.value)} - placeholder="Enter destination" - /> -
    + setRoute(routeCoordinates); + setSummary({ + length: routeSummary.length, + duration: routeSummary.duration, + date: format(new Date(), "yyyy_MM_dd"), + }); + setInstructions(routeInstructions); - {/* Transport Mode Selector */} -
    - - -
    + // Update search history + const updatedHistory = [destination, ...searchHistory.filter((item) => item !== destination)].slice(0, 10); // Limit to 10 items + setSearchHistory(updatedHistory); + localStorage.setItem("searchHistory", JSON.stringify(updatedHistory)); + + navigate("/navigation", { + state: { + originCoords: latLng(originCoords.lat, originCoords.lon), + destinationCoords: latLng(destinationCoords.lat, destinationCoords.lon), + route: routeCoordinates, + summary: { + length: routeSummary.length, + duration: routeSummary.duration, + date: format(new Date(), "yyyy_MM_dd"), + }, + instructions: routeInstructions, + litStreets: litStreets.map(({ lat, lon }: { lat: number; lon: number }) => [lat, lon] as LatLngTuple), + sidewalks, + policeStations: policeStations.map(({ lat, lon }: { lat: number; lon: number }) => [lat, lon] as LatLngTuple), + hospitals: hospitals.map(({ lat, lon }: { lat: number; lon: number }) => [lat, lon] as LatLngTuple), + theme: "standard", // Default theme passed; can be changed in JourneyPage + destinationName: destination, + }, + }); + } catch (err) { + console.error("Error during geocoding or fetching route:", err); + setError("Failed to find the location or route. Please try again."); + } + }; + + const clearHistory = () => { + setSearchHistory([]); + localStorage.removeItem("searchHistory"); + }; - {/* Map Theme Selector */} -
    - - + const handleHistoryClick = (item: string) => { + setDestination(item); + }; + + return ( +
    + {/* Destination Input */} +
    + setDestination(e.target.value)} + placeholder="Search here" + /> +
    + + {/* Transport Mode Selector */} +
    + + +
    + + {/* Search Button */} + + + {/* Error Message */} + {error &&

    {error}

    } + + {/* Tabs for Search History and Favorites */} +
    + + +
    + + {/* Tab Content */} +
    + {activeTab === "history" && searchHistory.length > 0 && ( +
    +
      + {searchHistory.map((item, index) => { + const isFavoritedItem = favoritePlaces.includes(item); + return ( +
    • + + +
    • + ); + })} +
    + +
    + )} + + {activeTab === "favorites" && favoritePlaces.length > 0 && ( +
    +
      + {favoritePlaces.map((item, index) => ( +
    • + +
    • + ))} +
    +
    + )} +
    + + {/* Map Display */} + {route.length > 0 && ( +
    + +
    + )}
    + ); + }; - {/* Search Button */} - - - {/* Error Message */} - {error &&

    {error}

    } - - {/* Map Display */} - {route.length > 0 && ( - - )} -
    - ); -}; - -export default WhereToPage; \ No newline at end of file + export default WhereToPage; \ No newline at end of file diff --git a/client/src/pages/journeyPage/AlarmButton.tsx b/client/src/pages/journeyPage/AlarmButton.tsx index 528fdff..25fc79f 100644 --- a/client/src/pages/journeyPage/AlarmButton.tsx +++ b/client/src/pages/journeyPage/AlarmButton.tsx @@ -1,33 +1,74 @@ -import { useState } from 'react'; +import { useState, useEffect, useRef } from "react"; +import { AiOutlineSound } from "react-icons/ai"; +import { IoVolumeMuteOutline } from "react-icons/io5"; const AlarmButton: React.FC = () => { - const [isPlaying, setIsPlaying] = useState(false); - const alarmSound = new Audio('/path/'); + const [isPlaying, setIsPlaying] = useState(false); + const alarmSoundRef = useRef(null); + + useEffect(() => { + // Initialize the alarm sound only once + alarmSoundRef.current = new Audio("/alarm.mp3"); + alarmSoundRef.current.loop = true; + + return () => { + // Clean up the audio when the component unmounts + if (alarmSoundRef.current) { + alarmSoundRef.current.pause(); + } + }; + }, []); - // Play the alarm sound const playAlarm = () => { - alarmSound.loop = true; //loop here ensures the alarm is on until manually stopped - alarmSound.play(); - setIsPlaying(true); + if (alarmSoundRef.current) { + alarmSoundRef.current.play().catch((err) => console.error("Audio playback error:", err)); + setIsPlaying(true); + } }; - // Stop the alarm sound const stopAlarm = () => { - alarmSound.pause(); - alarmSound.currentTime = 0; // Reset the playback position - setIsPlaying(false); + if (alarmSoundRef.current) { + alarmSoundRef.current.pause(); + alarmSoundRef.current.currentTime = 0; // Reset the playback position + setIsPlaying(false); + } + }; + + const toggleAlarm = () => { + if (isPlaying) { + stopAlarm(); + } else { + playAlarm(); + } + }; + + const iconStyle = { + fontSize: "40px", + color: "#ebebeb", + cursor: "pointer", + transition: "transform 0.2s ease, filter 0.3s ease", }; return (
    {isPlaying ? ( - + ) : ( - + )}
    ); diff --git a/client/src/pages/journeyPage/JourneyPage.tsx b/client/src/pages/journeyPage/JourneyPage.tsx index daf7a60..138b08b 100644 --- a/client/src/pages/journeyPage/JourneyPage.tsx +++ b/client/src/pages/journeyPage/JourneyPage.tsx @@ -1,19 +1,26 @@ -// Import necessary modules and components -import { useEffect, useState } from "react"; +// my file +import { useCallback, useEffect, useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import MapComponent from "../../components/MapComponent/MapComponent"; import ProgressBar from "./ProgressBar"; import SosButton from "./SosButton"; import AlarmButton from "./AlarmButton"; import WeatherInfo from "./WeatherInfo"; -import { usePosition } from "../../hooks/usePosition"; +import { PositionI, usePosition } from "../../hooks/usePosition"; import { fetchRoute } from "../../services/RoutingService"; -import { latLng } from "leaflet"; +import { latLng, LatLngTuple } from "leaflet"; import { decode } from "@here/flexpolyline"; -import { LatLngTuple } from "leaflet"; import "../../styles/JourneyPage.css"; -import { useSocket } from "../../hooks/useSocket"; import { createShare } from "../../services/shareService"; +import { RouteI, RouteRequestI } from "../../Types/Route"; +import { fetchInfrastructureData, processInfrastructureData } from "../../services/overpassService"; +import mapThemes from "../../components/MapComponent/MapThemes"; +import { useSocket } from "../../hooks/useSocket"; +import { TbNavigationCancel } from "react-icons/tb"; +import Footer from "../../components/Footer" +import { MdEmergencyShare } from "react-icons/md"; + + const JourneyPage: React.FC = () => { // React router hooks to access location state and navigate @@ -29,54 +36,104 @@ const JourneyPage: React.FC = () => { route: initialRoute = [], summary: initialSummary = { distance: 0, duration: 0 }, instructions: initialInstructions = [], - theme = "standard", + theme: initialTheme = "dark", transportMode = "pedestrian", destinationCoords = null, } = location.state || {}; // All state variables used - const [currentRoute, setCurrentRoute] = useState(initialRoute); // Active route + const [currentPolyline, setcurrentPolyline] = useState(initialRoute); // Active route const [currentSummary, setCurrentSummary] = useState(initialSummary); // Route summary const [currentInstructions, setCurrentInstructions] = useState(initialInstructions); // Instructions const [userDeviationDetected, setUserDeviationDetected] = useState(false); // Track route deviation const [rerouted, setRerouted] = useState(false); // Track if rerouting occurred + const [shareId, setShareId] = useState((localStorage.getItem("shareId") || "")); + const [litStreets, setLitStreets] = useState([]); + const [sidewalks, setSidewalks] = useState<{ geometry: { lat: number; lon: number }[] }[]>([]); + const [policeStations, setPoliceStations] = useState([]); + const [hospitals, setHospitals] = useState([]); + const [mapTheme, setMapTheme] = useState(initialTheme); + const { + isConnected, + sendPosition, + connectSocket, + hostShare, + } = useSocket({}); + + useEffect(() => { + connectSocket(); + // don't include in dev dependencies! + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (latitude && longitude) { + const southWest: [number, number] = [latitude - 0.01, longitude - 0.01]; + const northEast: [number, number] = [latitude + 0.01, longitude + 0.01]; + + fetchInfrastructureData(southWest, northEast) + .then((data) => { + const { litStreets, sidewalks, policeStations, hospitals } = processInfrastructureData(data); + setLitStreets(litStreets.map(({ lat, lon }) => [lat, lon] as LatLngTuple)); + setSidewalks(sidewalks); + setPoliceStations(policeStations.map(({ lat, lon }) => [lat, lon] as LatLngTuple)); + setHospitals(hospitals.map(({ lat, lon }) => [lat, lon] as LatLngTuple)); + }) + .catch((err) => console.error("Error fetching infrastructure data:", err)); + } + }, [latitude, longitude]); + + const handleThemeChange = (newTheme: string) => setMapTheme(newTheme); // Detect if the user deviates from the route useEffect(() => { - if (latitude && longitude && currentRoute.length > 0) { + if (latitude && longitude && currentPolyline.length > 0) { const userLocation = latLng(latitude, longitude); // Check if the user is close to any point on the route - const isOnRoute = currentRoute.some(([lat, lon]: LatLngTuple) => { + const isOnRoute = currentPolyline.some(([lat, lon]: LatLngTuple) => { const point = latLng(lat, lon); return userLocation.distanceTo(point) <= 50; // Deviation threshold in meters }); setUserDeviationDetected(!isOnRoute); // Update deviation state } - }, [latitude, longitude, currentRoute]); + }, [latitude, longitude, currentPolyline]); + + + + const handleSOSActivated = () => { + console.log('SOS button activated on Journey Page!'); + + }; // Handle rerouting if the user deviates from the route - const handleReroute = async () => { - if (!latitude || !longitude || currentRoute.length === 0) return; + const handleReroute = useCallback(async () => { + + if (!latitude || !longitude || currentPolyline.length === 0) return; try { // Get the last point in the route as the destination - const destination = currentRoute[currentRoute.length - 1]; + const destination = currentPolyline[currentPolyline.length - 1]; const [destLat, destLon] = destination; + const request: RouteRequestI = { + origin: [latitude, longitude], + destination: [destLat, destLon], + transportMode + } + + // Fetch new route, summary, and instructions from the routing service const { polyline, summary, instructions } = await fetchRoute( - [latitude, longitude], - [destLat, destLon], - transportMode + request ); // Decode the polyline into LatLng tuples const decoded = decode(polyline); if (decoded && Array.isArray(decoded.polyline)) { const newRoute: LatLngTuple[] = decoded.polyline.map(([lat, lon]) => [lat, lon] as LatLngTuple); - setCurrentRoute(newRoute); // Update the route + setcurrentPolyline(newRoute); // Update the route setCurrentSummary(summary); // Update summary setCurrentInstructions(instructions); // Update instructions setRerouted(true); // Mark rerouting as done @@ -84,12 +141,16 @@ const JourneyPage: React.FC = () => { } catch (error) { console.error("Error during rerouting:", error); // Log rerouting errors } - }; + + }, [latitude, longitude, currentPolyline, transportMode]) // Trigger rerouting when deviation is detected - useEffect(() => { - if (userDeviationDetected) handleReroute(); - }, [userDeviationDetected]); +useEffect(() => { + const handleDeviation = () => { + if (userDeviationDetected) console.log('user deviation detected"') + } + handleDeviation(); + }, [userDeviationDetected]) // Announce turn-by-turn instructions using audio const announceTurn = (instruction: string) => { @@ -107,84 +168,144 @@ const JourneyPage: React.FC = () => { }, [currentInstructions]); // Socket-related hooks and functions - const { error: socketError, connectSocket, hostShare, sendPosition } = useSocket({}); - async function handleShare() { + async function handleShare () { try { - // Convert LatLng to string - const originCoords = `${currentRoute[0][0]},${currentRoute[0][1]}`; - const destinationCoordsFormatted = - destinationCoords || `${currentRoute[currentRoute.length - 1][0]},${currentRoute[currentRoute.length - 1][1]}`; - - const result = await createShare({ origin: originCoords, destination: destinationCoordsFormatted }); - - if (result.id) { - connectSocket(); - if (!socketError) hostShare(result.id); + if (shareId) { + alert("Active share already in progress"); + return; + } + const route: RouteI = {polyline: currentPolyline, instructions: currentInstructions, summary: currentSummary}; + console.log('sharing' , route) + const result = await createShare(route); + const url = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ":" + window.location.port : "") + "/observe/" + result.data.id + "?password=" + result.data.password; + console.log(`UPDATED PAGE: ${url}`) + setShareId(result.data.id); + const data = { + title: "Shared Location", + text: "Please follow the link to access the shared location", + url + }; + if ("share" in navigator) { + await navigator.share(data); + } else { + alert("data"); } } catch (err) { - console.error("Error during sharing:", err); + console.error("Error during sharing: ", err); } } +/* function handleCancel () { + localStorage.removeItem("shareId"); + navigate("/"); + } */ + + + + + useEffect(() => { - if (socketError) { - console.error("Socket error:", socketError); - } else { - sendPosition(position); + console.log('connected?', isConnected) + if (isConnected && shareId) { + hostShare(shareId); } + }, [isConnected, shareId, hostShare]); + useEffect(() => { + console.log("Updated position:", position); }, [position]); + useEffect(() => { + if (isConnected) { + console.log("Trying to send position to share"); + const typedPosition: PositionI = position; + console.log('my typed position', typedPosition); + sendPosition(typedPosition); + console.log('this may have been sent.') + } + }, [isConnected, position, sendPosition, shareId]); + // Rendering return ( -
    - {/* Render the map component if the route is available */} - {currentRoute.length > 0 ? ( - - ) : ( -

    Loading route...

    - )} - - {/* Display the next turn instruction */} -
    -

    Next Turn:

    -

    {currentInstructions[0]?.instruction || "Continue straight"}

    +
    + { + currentPolyline.length > 0 ? ( + + ) : ( +

    Loading route...

    + )} + +
    +

    Next Turn:

    + < p > {currentInstructions[0]?.instruction || "Continue straight"}

    - {/* Progress Bar Section */} - - - {/* Buttons and Features Section */} -
    - - - - - + < ProgressBar progress={rerouted ? 75 : 50} /> + +
    + + < AlarmButton /> + + navigate("/")} style={{ + fontSize: "40px", + color: "#EBEBEB", + cursor: "pointer", + transition: "transform 0.2s ease", + }} title="Cancel Journey" /> + +
    + Map Theme Icon + < select + id="map-theme" + value={mapTheme} + onChange={(e) => handleThemeChange(e.target.value)} + > + { + Object.keys(mapThemes).map((themeKey) => ( + + )) + } + +
    +
    - {/* Display weather information */} - + < WeatherInfo /> +
    ); }; diff --git a/client/src/pages/journeyPage/SosButton.tsx b/client/src/pages/journeyPage/SosButton.tsx index 6314ac4..3fe770e 100644 --- a/client/src/pages/journeyPage/SosButton.tsx +++ b/client/src/pages/journeyPage/SosButton.tsx @@ -1,16 +1,184 @@ +import React, { useState } from 'react'; +import "../../styles/SosButton.css" +interface SOSButtonProps { + onSOSActivated?: () => void; // Optional callback for additional logic when SOS is activated +} -const SosButton: React.FC = () => { - const handleSOS = () => { - alert('SOS Triggered! Sending help...'); - // we need to add more SOS functionality? +const base_URL = import.meta.env.VITE_BACKEND_URL +if (!base_URL) { + console.error('NO URL FOUND') +} + + +const SOSButton: React.FC = ({ onSOSActivated }) => { + const [showContactSelector, setShowContactSelector] = useState(false); + const [selectedContact, setSelectedContact] = useState(null); + const sosSettings = JSON.parse(localStorage.getItem('settings') || '{}'); + const sosMessage = sosSettings.sosMessage || 'Help me, I am in danger!'; + + const handleSOSClick = async () => { + if (!sosSettings) { + alert('SOS settings not found! Please configure them in settings.'); + return; + } + + // Check if emergency contacts are present + if (sosSettings.notifyContacts) { + if (sosSettings.emergencyContacts?.length > 0) { + setShowContactSelector(true); // Show contact selector popup + } else { + alert('No emergency contacts found!'); + } + } + + if (sosSettings.notifyNearbyUsers) { + await notifyNearbyUsers(); + } + + if (sosSettings.callAuthorities) { + callAuthorities(); + } + + // Optional callback + if (onSOSActivated) { + onSOSActivated(); + } + }; + + const handleSendSOS = async () => { + if (selectedContact) { + await notifyEmergencyContacts([selectedContact], sosMessage); + alert(`SOS message sent to ${selectedContact}`); + setShowContactSelector(false); + } else { + alert('Please select a contact to send the SOS message.'); + } + }; + + const notifyEmergencyContacts = async (contacts: string[], message: string) => { + try { + const response = await fetch(`${base_URL}/notify-contacts`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ contacts, message }), + }); + + if (!response.ok) { + throw new Error('Failed to send SMS notifications'); + } + + const result = await response.json(); + console.log('SMS notifications sent successfully:', result); + alert('SMS notifications sent successfully!'); + } catch (error) { + console.error('Error notifying emergency contacts:', error); + alert('Failed to send SMS notifications. Please try again.'); + } + }; + + const notifyNearbyUsers = async () => { + console.log('Notifying nearby SafeWalk users...'); + // Add WebSocket or real-time notification logic + alert('Mock notification sent to nearby users.'); + return Promise.resolve(); + }; + + const callAuthorities = () => { + console.log('Calling local authorities...'); + alert('Mock call to emergency number triggered.'); + window.open('tel:112'); // Replace 112 with your emergency number }; return ( - +
    + {/* SOS Button */} + + + {/* Contact Selector Popup */} + {showContactSelector && ( +
    +

    Select Emergency Contact

    +
      + {sosSettings.emergencyContacts?.map((contact: string, index: number) => ( +
    • + +
    • + ))} +
    +
    + + +
    +
    + )} +
    ); }; -export default SosButton; \ No newline at end of file +export default SOSButton; \ No newline at end of file diff --git a/client/src/pages/observerPage/ObserverPage.tsx b/client/src/pages/observerPage/ObserverPage.tsx index ba05e27..72fe59a 100644 --- a/client/src/pages/observerPage/ObserverPage.tsx +++ b/client/src/pages/observerPage/ObserverPage.tsx @@ -1,31 +1,100 @@ +import { useEffect, useState } from 'react'; import { useParams, useSearchParams } from 'react-router-dom'; -//import MapComponent from '../../components/MapComponent/MapComponent'; -import { useSocket } from '../../hooks/useSocket'; -import { useEffect } from 'react'; -//import { ShareI } from '../../services/shareService'; +import { useSocket } from '../../hooks/useSocket.js'; +import { latLng } from 'leaflet'; +import MapComponent from '../../components/MapComponent/MapComponent.js'; +import { accessShare } from '../../services/shareService.js'; +import { RouteI } from '../../Types/Route.js'; +import "../../styles/ObserverPage.css"; +import { ShareI } from '../../Types/Share.js'; +import { AxiosResponse } from 'axios'; const ObserverPage = () => { - //const [share, setShare] = useState(); + const [route, setRoute] = useState({polyline: [], instructions: [], summary: {duration: 0, length: 0, date: ""}}); + const [sharerName, setSharerName] = useState(""); const [searchParams] = useSearchParams(); - const { id } = useParams(); + const { id } = useParams() || ""; const password = searchParams.get("password") || ""; - const { position, connectSocket, joinShare } = useSocket({password}); + const { isConnected, connectSocket, position, error, joinShare } = useSocket({ + password, + }); + + // Fetch shared route on component load + /* useEffect(() => { + const fetchRoute = async () => { + if (!id || !password) return; + try { + const result: PartialShare = await accessShare(id as string, password); + if (result?.data?.route) { + console.log("Fetched route from API:", result.data.route); + setRoute(result.data.route); + } + } catch (err) { + console.error("Error fetching route:", err); + } + }; + + fetchRoute(); + }, [id, password]); */ + + // Connect to WebSocket and join the share room + useEffect(() => { + connectSocket(); + if (id) joinShare(id); + }, [connectSocket, id, joinShare]); + + // Log WebSocket connection status and position updates useEffect(() => { - if (id) { - connectSocket(); - console.log("Connected to socket"); - joinShare(id); - console.log("Joined share " + id); + if (isConnected) { + console.log(`Connected to WebSocket with ID: ${id}`); } - }, []); + }, [id, isConnected]); -return ( - <> -

    {JSON.stringify(position)}

    - -); + async function getShareOnLoad () { + const result: AxiosResponse = await accessShare(id as string, password); + if (result.data) { + setRoute(result.data.route); + setSharerName(result.data.owner.firstName); + console.log("Set route to result of API request"); + console.log(result.data); + } + } + + useEffect(() => { + if (id && password) { + getShareOnLoad(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + },[]) + return ( +
    +
    {sharerName} has shared their position with you:
    + {error &&
    Error: {error}
    } + {position && route ? ( + <> + + +) :
    Waiting to receive position...
    } +
    + ); +}; -export default ObserverPage; \ No newline at end of file +export default ObserverPage diff --git a/client/src/pages/profilePage/InfoComponent.tsx b/client/src/pages/profilePage/InfoComponent.tsx new file mode 100644 index 0000000..9d05059 --- /dev/null +++ b/client/src/pages/profilePage/InfoComponent.tsx @@ -0,0 +1,249 @@ +import React, { useEffect, useState } from "react"; +import "../../styles/InfoComponent.css"; +import { useUser } from "../../hooks/useUser"; + +const InfoComponent: React.FC = () => { + const { user, updateUser } = useUser(); + + + const [editMode, setEditMode] = useState(false); + const [showModal, setShowModal] = useState(false); + const [reenteredPassword, setReenteredPassword] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + const [fieldBeingEdited, setFieldBeingEdited] = useState(null); + const [formData, setFormData] = useState({ + firstName: user?.firstName || "", + lastName: user?.lastName || "", + email: user?.email || "", + password: "**********", + telephone: user?.telephone || "", + }); + + useEffect(() => { + if (user) { + setFormData({ + firstName: user.firstName || "", + lastName: user.lastName || "", + email: user.email || "", + password: "**********", + telephone: user.telephone || "", + }); + } + }, [user]); + + const toggleEditMode = () => { + setEditMode(!editMode); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData({ + ...formData, + [name]: value, + }); + }; + + const handleSaveField = (fieldName: string) => { + setFieldBeingEdited(fieldName); + console.log(`[handleSaveField]: field is ${fieldName}`) + if (fieldName === "password") { + setShowModal(true); + } else { + submitData(fieldName); + } + }; + + const handleReenteredPasswordChange = (e: React.ChangeEvent) => { + setReenteredPassword(e.target.value); + }; + + const submitData = async (fieldName: string) => { + console.log(`[submitData]: fieldName is ${fieldName}`) + if (!user) { + console.log('no user'); + return}; + + try { + const payload: { + [key in keyof typeof formData]?: string; + } & { _id: string } = { + [fieldName]: formData[fieldName as keyof typeof formData], + _id: user._id, + + }; + + if (fieldName === "password") { + payload.password = reenteredPassword; + await updateUser({ password: reenteredPassword }); + } + + await updateUser(payload); + + + } catch (error) { + console.error("Error updating profile:", error); + setErrorMessage("Failed to update profile. Please try again."); + } + + setReenteredPassword(""); + }; + + return ( +
    + {!editMode ? ( + <> + + + + + + + + + + + + + + + + + + + + + + + +
    First Name:{user && user.firstName}
    Last Name:{user && user.lastName}
    Email:{user && user.email}
    Password:********
    Phone:{user && user.telephone}
    + + + ) : ( +
    + {/* First Name Field */} +
    + + + +
    + + {/* Last Name Field */} +
    + + + +
    + + {/* Email Field */} +
    + + + +
    + + {/* Password Field */} +
    + + + +
    + + {/* Phone Field */} +
    + + + +
    + +
    + )} + {showModal && ( +
    +
    +

    Confirm Your Password

    +

    Please re-enter your password to confirm changes:

    + + {errorMessage &&

    {errorMessage}

    } + + +
    +
    + )} +
    + ); +}; + +export default InfoComponent; \ No newline at end of file diff --git a/client/src/pages/profilePage/LoginComponent.tsx b/client/src/pages/profilePage/LoginComponent.tsx new file mode 100644 index 0000000..ebed3b7 --- /dev/null +++ b/client/src/pages/profilePage/LoginComponent.tsx @@ -0,0 +1,79 @@ +import React, { useContext, useState } from "react"; +import { loginService } from "../../services/authService"; +import "../../styles/RegisterComponent.css"; // Reuse the same styles +import { AuthContext } from "../../contexts/UserContext"; + +interface LoginComponentProps { + setViewOption: (view: string) => void; +} + +const LoginPage: React.FC = ({ setViewOption }) => { + const [formData, setFormData] = useState({ email: "", password: "" }); + const [message, setMessage] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + const {handleLoginContext} = useContext(AuthContext) + + + const handleChange = (event: React.ChangeEvent) => { + setFormData({ ...formData, [event.target.name]: event.target.value }); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setMessage(null); + setErrorMessage(null); + try { + const response = await loginService(formData); + if (response.data.token && response.data.user) { + handleLoginContext(response.data.token, response.data.user) + setMessage("Login successful!"); + setTimeout(() => 1); + setViewOption("settings"); + } else { + setErrorMessage("Please check your credentials."); + } + } catch (error) { + console.error("Login error:", error); + setErrorMessage("An error occurred. Please try again."); + } + }; + + return ( +
    + +
    +

    Login to GlowPath:

    +
    + + +
    +
    + + +
    + + {message &&

    {message}

    } + {errorMessage &&

    {errorMessage}

    } +
    +
    + ); +}; + +export default LoginPage; \ No newline at end of file diff --git a/client/src/pages/profilePage/ProfilePage.tsx b/client/src/pages/profilePage/ProfilePage.tsx new file mode 100644 index 0000000..8e5c621 --- /dev/null +++ b/client/src/pages/profilePage/ProfilePage.tsx @@ -0,0 +1,54 @@ +import { useContext, useEffect, useState } from "react"; +import SettingsComponent from "./SettingsComp"; +import { AuthContext } from "../../contexts/UserContext"; +import RegisterComponent from "./RegisterComponent"; +import "../../styles/profilePage.css"; +import "../../styles/Footer.css"; +import LoginComponent from "./LoginComponent"; +import Footer from "../../components/Footer" +import InfoComponent from "./InfoComponent"; +import { useUser } from "../../hooks/useUser"; + +const ProfilePage = () => { + const [viewOption, setViewOption] = useState(""); + const { isAuthorized, handleLogoutContext } = useContext(AuthContext); + const {user} = useUser() + + useEffect(() => { + if (isAuthorized === true && user) { + setViewOption("settings"); + + } + }, [isAuthorized, user]); + + const logout = () => { + handleLogoutContext(); + setViewOption("login"); + }; + + return ( +
    +
    + + + {/* + + */} +
    + +
    +

    Welcome, {user?.firstName}!

    + +
    + {viewOption === "settings" && } + {viewOption === "info" && } + {viewOption === "" && } + {viewOption === "login" && } +
    +
    +
    +
    + ); +}; + +export default ProfilePage; diff --git a/client/src/pages/profilePage/RegisterComponent.tsx b/client/src/pages/profilePage/RegisterComponent.tsx new file mode 100644 index 0000000..a206f88 --- /dev/null +++ b/client/src/pages/profilePage/RegisterComponent.tsx @@ -0,0 +1,127 @@ +import React, { FormEvent, useContext, useState } from "react"; +import { registerService } from "../../services/authService"; +import '../../styles/RegisterComponent.css'; +import 'react-phone-number-input/style.css'; +import PhoneInput from "react-phone-number-input/input"; +import { AuthContext } from "../../contexts/UserContext"; + + + + +interface RegisterComponentProps { + setViewOption: (view: string) => void; +} + + +const RegisterComponent: React.FC + = ({setViewOption}) => { + const [formData, setFormData] = useState({ + firstName: "", + lastName: "", + email: "", + password: "", + telephone: "", + + }); + const [message, setMessage] = useState(""); + const [value, setValue] = useState(); + const {handleLoginContext} = useContext(AuthContext) + + + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + const handlePhoneChange = (value: string | undefined) => { + setValue(value); // Update the phone input value + setFormData({ ...formData, telephone: value || "" }); + }; + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + const password = formData.password; + if (password.length < 8) { + alert('Password must be at least 8 characters long'); + return; + } + try { + const response = await registerService(formData); + console.log(response) + if (response?.data.token && response.data.user) { + handleLoginContext(response.data.token, response.data.user) + setMessage("Registration successful!"); + setTimeout(() => 1); + setViewOption('settings') + } + + } catch (error) { + setMessage("Error registering. Please try again. " + error); + } + }; + + return ( + <> + +
    + +
    +

    Register for GlowPath:

    +
    + + + + + + + + + +
    +
    + + +
    + + + {message &&

    {message}

    } +
    + +
    + + ); +}; + +export default RegisterComponent +; diff --git a/client/src/pages/profilePage/SettingsComp.tsx b/client/src/pages/profilePage/SettingsComp.tsx new file mode 100644 index 0000000..1038bd3 --- /dev/null +++ b/client/src/pages/profilePage/SettingsComp.tsx @@ -0,0 +1,113 @@ +import React, { useState, useEffect } from "react"; +import "../../styles/SettingsPage.css"; +import { useUser } from "../../hooks/useUser"; +import { SettingsI } from "../../Types/User"; + + +const SettingsPage: React.FC = () => { +/* const defaultSettings: SettingsI = { + notifyNearby: true, + notifyAuthorities: true, + allowNotifications: true, + defaultSos: "Please contact me, I might be in danger.", + theme: "dark" + }; */ + const { user, updateUser } = useUser(); + const settings: SettingsI | undefined = user?.settings; + console.log(`settingg is ${settings}`) + + const [uiNotifyNearby, setUiNotifyNearby] = useState(true); + const [uiNotifyAuthorities, setUiNotifyAuthorities] = useState(true); + const [uiAllowNotifications, setUiAllowNotifications] = useState(true); + const [uiSosMessage, setUiSosMessage] = useState(user?.settings?.defaultSos || "Help me, I am in danger!"); + const [uiTheme, setUiTheme] = useState("dark"); + + // Load settings from hook + useEffect(() => { +if (settings) { + setUiNotifyNearby(settings.notifyNearby); + setUiNotifyAuthorities(settings.notifyAuthorities); + setUiAllowNotifications(settings.allowNotifications); + setUiSosMessage(settings.defaultSos); + setUiTheme(settings.theme || "dark"); +} + }, [settings]); + + // Apply theme to document body + useEffect(() => { + document.documentElement.setAttribute("data-theme", uiTheme); + }, [uiTheme]); + + const toggleTheme = () => { + setUiTheme((prev) => (prev === "dark" ? "light" : "dark")); + }; + + const handleSave = async () => { + const newSettings: SettingsI = { + notifyNearby: uiNotifyNearby, + notifyAuthorities: uiNotifyAuthorities, + allowNotifications: uiAllowNotifications, + defaultSos: uiSosMessage, + theme: uiTheme, + }; + + await updateUser({settings: newSettings}); + alert("Settings saved!"); + }; + + return ( +
    +

    Settings

    + +
    +

    SOS Button Options

    + + + +
    + +
    +

    Default SOS Message

    +