diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml
index c3b1647..ac77f52 100644
--- a/.github/workflows/firebase-hosting-merge.yml
+++ b/.github/workflows/firebase-hosting-merge.yml
@@ -13,11 +13,18 @@ jobs:
steps:
- uses: actions/checkout@v4
- # ๐ Inject secret into build env
+ # โ
Inject secrets into build environment
- name: Install and Build
run: npm ci && npm run build
env:
REACT_APP_MAPBOX_ACCESS_TOKEN: ${{ secrets.REACT_APP_MAPBOX_ACCESS_TOKEN }}
+ REACT_APP_FIREBASE_API_KEY: ${{ secrets.REACT_APP_FIREBASE_API_KEY }}
+ REACT_APP_FIREBASE_AUTH_DOMAIN: ${{ secrets.REACT_APP_FIREBASE_AUTH_DOMAIN }}
+ REACT_APP_FIREBASE_PROJECT_ID: ${{ secrets.REACT_APP_FIREBASE_PROJECT_ID }}
+ REACT_APP_FIREBASE_STORAGE_BUCKET: ${{ secrets.REACT_APP_FIREBASE_STORAGE_BUCKET }}
+ REACT_APP_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.REACT_APP_FIREBASE_MESSAGING_SENDER_ID }}
+ REACT_APP_FIREBASE_APP_ID: ${{ secrets.REACT_APP_FIREBASE_APP_ID }}
+ REACT_APP_FIREBASE_MEASUREMENT_ID: ${{ secrets.REACT_APP_FIREBASE_MEASUREMENT_ID }}
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml
index 732362c..629a5e0 100644
--- a/.github/workflows/firebase-hosting-pull-request.yml
+++ b/.github/workflows/firebase-hosting-pull-request.yml
@@ -15,11 +15,18 @@ jobs:
steps:
- uses: actions/checkout@v4
- # ๐ Inject secret for PR build
+ # โ
Inject secrets for PR build
- name: Install and Build
run: npm ci && npm run build
env:
REACT_APP_MAPBOX_ACCESS_TOKEN: ${{ secrets.REACT_APP_MAPBOX_ACCESS_TOKEN }}
+ REACT_APP_FIREBASE_API_KEY: ${{ secrets.REACT_APP_FIREBASE_API_KEY }}
+ REACT_APP_FIREBASE_AUTH_DOMAIN: ${{ secrets.REACT_APP_FIREBASE_AUTH_DOMAIN }}
+ REACT_APP_FIREBASE_PROJECT_ID: ${{ secrets.REACT_APP_FIREBASE_PROJECT_ID }}
+ REACT_APP_FIREBASE_STORAGE_BUCKET: ${{ secrets.REACT_APP_FIREBASE_STORAGE_BUCKET }}
+ REACT_APP_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.REACT_APP_FIREBASE_MESSAGING_SENDER_ID }}
+ REACT_APP_FIREBASE_APP_ID: ${{ secrets.REACT_APP_FIREBASE_APP_ID }}
+ REACT_APP_FIREBASE_MEASUREMENT_ID: ${{ secrets.REACT_APP_FIREBASE_MEASUREMENT_ID }}
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
diff --git a/package-lock.json b/package-lock.json
index 90ef6da..e257384 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^13.5.0",
+ "firebase": "^11.6.0",
"mapbox-gl": "^3.10.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -2473,6 +2474,692 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@firebase/analytics": {
+ "version": "0.10.12",
+ "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.12.tgz",
+ "integrity": "sha512-iDCGnw6qdFqwI5ywkgece99WADJNoymu+nLIQI4fZM/vCZ3bEo4wlpEetW71s1HqGpI0hQStiPhqVjFxDb2yyw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.13",
+ "@firebase/installations": "0.6.13",
+ "@firebase/logger": "0.4.4",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/analytics-compat": {
+ "version": "0.2.18",
+ "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.18.tgz",
+ "integrity": "sha512-Hw9mzsSMZaQu6wrTbi3kYYwGw9nBqOHr47pVLxfr5v8CalsdrG5gfs9XUlPOZjHRVISp3oQrh1j7d3E+ulHPjQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/analytics": "0.10.12",
+ "@firebase/analytics-types": "0.8.3",
+ "@firebase/component": "0.6.13",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/analytics-types": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz",
+ "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/app": {
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.11.4.tgz",
+ "integrity": "sha512-GPREsZjfSaHzwyC6cI/Cqvzf6zxqMzya+25tSpUstdqC2w0IdfxEfOMjfdW7bDfVEf4Rb4Nb6gfoOAgVSp4c4g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.13",
+ "@firebase/logger": "0.4.4",
+ "@firebase/util": "1.11.0",
+ "idb": "7.1.1",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@firebase/app-check": {
+ "version": "0.8.13",
+ "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.13.tgz",
+ "integrity": "sha512-ONsgml8/dplUOAP42JQO6hhiWDEwR9+RUTLenxAN9S8N6gel/sDQ9Ci721Py1oASMGdDU8v9R7xAZxzvOX5lPg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.13",
+ "@firebase/logger": "0.4.4",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/app-check-compat": {
+ "version": "0.3.20",
+ "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.20.tgz",
+ "integrity": "sha512-/twgmlnNAaZ/wbz3kcQrL/26b+X+zUX+lBmu5LwwEcWcpnb+mrVEAKhD7/ttm52dxYiSWtLDeuXy3FXBhqBC5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/app-check": "0.8.13",
+ "@firebase/app-check-types": "0.5.3",
+ "@firebase/component": "0.6.13",
+ "@firebase/logger": "0.4.4",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/app-check-interop-types": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz",
+ "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/app-check-types": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz",
+ "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/app-compat": {
+ "version": "0.2.53",
+ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.53.tgz",
+ "integrity": "sha512-vDeZSit0q4NyaDIVcaiJF3zhLgguP6yc0JwQAfpTyllgt8XMtkMFyY/MxJtFrK2ocpQX/yCbV2DXwvpY2NVuJw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/app": "0.11.4",
+ "@firebase/component": "0.6.13",
+ "@firebase/logger": "0.4.4",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@firebase/app-types": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz",
+ "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/auth": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.0.tgz",
+ "integrity": "sha512-S7SqBsN7sIQsftNE3bitLlK+4bWrTHY+Rx2JFlNitgVYu2nK8W8ZQrkG8GCEwiFPq0B2vZ9pO5kVTFfq2sP96A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.13",
+ "@firebase/logger": "0.4.4",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x",
+ "@react-native-async-storage/async-storage": "^1.18.1"
+ },
+ "peerDependenciesMeta": {
+ "@react-native-async-storage/async-storage": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@firebase/auth-compat": {
+ "version": "0.5.20",
+ "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.20.tgz",
+ "integrity": "sha512-8FwODTSBnaqGQbKfML7LcpzGGPyouB7YHg3dZq+CZMziVc7oBY1jJeNvpnM1hAQoVuTjWPXoRrCltdGeOlkKfQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/auth": "1.10.0",
+ "@firebase/auth-types": "0.13.0",
+ "@firebase/component": "0.6.13",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/auth-interop-types": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz",
+ "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/auth-types": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz",
+ "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@firebase/app-types": "0.x",
+ "@firebase/util": "1.x"
+ }
+ },
+ "node_modules/@firebase/component": {
+ "version": "0.6.13",
+ "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.13.tgz",
+ "integrity": "sha512-I/Eg1NpAtZ8AAfq8mpdfXnuUpcLxIDdCDtTzWSh+FXnp/9eCKJ3SNbOCKrUCyhLzNa2SiPJYruei0sxVjaOTeg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@firebase/data-connect": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.3.tgz",
+ "integrity": "sha512-JsgppNX1wcQYP5bg4Sg6WTS7S0XazklSjr1fG3ox9DHtt4LOQwJ3X1/c81mKMIZxocV22ujiwLYQWG6Y9D1FiQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/auth-interop-types": "0.2.4",
+ "@firebase/component": "0.6.13",
+ "@firebase/logger": "0.4.4",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/database": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.14.tgz",
+ "integrity": "sha512-9nxYtkHAG02/Nh2Ssms1T4BbWPPjiwohCvkHDUl4hNxnki1kPgsLo5xe9kXNzbacOStmVys+RUXvwzynQSKmUQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/app-check-interop-types": "0.3.3",
+ "@firebase/auth-interop-types": "0.2.4",
+ "@firebase/component": "0.6.13",
+ "@firebase/logger": "0.4.4",
+ "@firebase/util": "1.11.0",
+ "faye-websocket": "0.11.4",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@firebase/database-compat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.0.5.tgz",
+ "integrity": "sha512-CNf1UbvWh6qIaSf4sn6sx2DTDz/em/D7QxULH1LTxxDQHr9+CeYGvlAqrKnk4ZH0P0eIHyQFQU7RwkUJI0B9gQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.13",
+ "@firebase/database": "1.0.14",
+ "@firebase/database-types": "1.0.10",
+ "@firebase/logger": "0.4.4",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@firebase/database-types": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.10.tgz",
+ "integrity": "sha512-mH6RC1E9/Pv8jf1/p+M8YFTX+iu+iHDN89hecvyO7wHrI4R1V0TXjxOHvX3nLJN1sfh0CWG6CHZ0VlrSmK/cwg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/app-types": "0.9.3",
+ "@firebase/util": "1.11.0"
+ }
+ },
+ "node_modules/@firebase/firestore": {
+ "version": "4.7.10",
+ "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.10.tgz",
+ "integrity": "sha512-6nKsyo2U+jYSCcSE5sjMdDNA23DMUvYPUvsYGg09CNvcTO8GGKsPs7SpOhspsB91mbacq+u627CDAx3FUhPSSQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.13",
+ "@firebase/logger": "0.4.4",
+ "@firebase/util": "1.11.0",
+ "@firebase/webchannel-wrapper": "1.0.3",
+ "@grpc/grpc-js": "~1.9.0",
+ "@grpc/proto-loader": "^0.7.8",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/firestore-compat": {
+ "version": "0.3.45",
+ "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.45.tgz",
+ "integrity": "sha512-uRvi7AYPmsDl7UZwPyV7jgDGYusEZ2+U2g7MndbQHKIA8fNHpYC6QrzMs58+/IjX+kF/lkUn67Vrr0AkVjlY+Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.13",
+ "@firebase/firestore": "4.7.10",
+ "@firebase/firestore-types": "3.0.3",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/firestore-types": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz",
+ "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@firebase/app-types": "0.x",
+ "@firebase/util": "1.x"
+ }
+ },
+ "node_modules/@firebase/functions": {
+ "version": "0.12.3",
+ "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.12.3.tgz",
+ "integrity": "sha512-Wv7JZMUkKLb1goOWRtsu3t7m97uK6XQvjQLPvn8rncY91+VgdU72crqnaYCDI/ophNuBEmuK8mn0/pAnjUeA6A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/app-check-interop-types": "0.3.3",
+ "@firebase/auth-interop-types": "0.2.4",
+ "@firebase/component": "0.6.13",
+ "@firebase/messaging-interop-types": "0.2.3",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/functions-compat": {
+ "version": "0.3.20",
+ "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.20.tgz",
+ "integrity": "sha512-iIudmYDAML6n3c7uXO2YTlzra2/J6lnMzmJTXNthvrKVMgNMaseNoQP1wKfchK84hMuSF8EkM4AvufwbJ+Juew==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.13",
+ "@firebase/functions": "0.12.3",
+ "@firebase/functions-types": "0.6.3",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/functions-types": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz",
+ "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/installations": {
+ "version": "0.6.13",
+ "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.13.tgz",
+ "integrity": "sha512-6ZpkUiaygPFwgVneYxuuOuHnSPnTA4KefLEaw/sKk/rNYgC7X6twaGfYb0sYLpbi9xV4i5jXsqZ3WO+yaguNgg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.13",
+ "@firebase/util": "1.11.0",
+ "idb": "7.1.1",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/installations-compat": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.13.tgz",
+ "integrity": "sha512-f/o6MqCI7LD/ulY9gvgkv6w5k6diaReD8BFHd/y/fEdpsXmFWYS/g28GXCB72bRVBOgPpkOUNl+VsMvDwlRKmw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.13",
+ "@firebase/installations": "0.6.13",
+ "@firebase/installations-types": "0.5.3",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/installations-types": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz",
+ "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@firebase/app-types": "0.x"
+ }
+ },
+ "node_modules/@firebase/logger": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.4.tgz",
+ "integrity": "sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@firebase/messaging": {
+ "version": "0.12.17",
+ "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.17.tgz",
+ "integrity": "sha512-W3CnGhTm6Nx8XGb6E5/+jZTuxX/EK8Vur4QXvO1DwZta/t0xqWMRgO9vNsZFMYBqFV4o3j4F9qK/iddGYwWS6g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.13",
+ "@firebase/installations": "0.6.13",
+ "@firebase/messaging-interop-types": "0.2.3",
+ "@firebase/util": "1.11.0",
+ "idb": "7.1.1",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/messaging-compat": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.17.tgz",
+ "integrity": "sha512-5Q+9IG7FuedusdWHVQRjpA3OVD9KUWp/IPegcv0s5qSqRLBjib7FlAeWxN+VL0Ew43tuPJBY2HKhEecuizmO1Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.13",
+ "@firebase/messaging": "0.12.17",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/messaging-interop-types": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz",
+ "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/performance": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.2.tgz",
+ "integrity": "sha512-DXLLp0R0jdxH/yTmv+WTkOzsLl8YYecXh4lGZE0dzqC0IV8k+AxpLSSWvOTCkAETze8yEU/iF+PtgYVlGjfMMQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.13",
+ "@firebase/installations": "0.6.13",
+ "@firebase/logger": "0.4.4",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0",
+ "web-vitals": "^4.2.4"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/performance-compat": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.15.tgz",
+ "integrity": "sha512-wUxsw7hGBEMN6XfvYQqwPIQp5LcJXawWM5tmYp6L7ClCoTQuEiCKHWWVurJgN8Q1YHzoHVgjNfPQAOVu29iMVg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.13",
+ "@firebase/logger": "0.4.4",
+ "@firebase/performance": "0.7.2",
+ "@firebase/performance-types": "0.2.3",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/performance-types": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz",
+ "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/performance/node_modules/web-vitals": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
+ "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/remote-config": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.6.0.tgz",
+ "integrity": "sha512-Yrk4l5+6FJLPHC6irNHMzgTtJ3NfHXlAXVChCBdNFtgmzyGmufNs/sr8oA0auEfIJ5VpXCaThRh3P4OdQxiAlQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.13",
+ "@firebase/installations": "0.6.13",
+ "@firebase/logger": "0.4.4",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/remote-config-compat": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.13.tgz",
+ "integrity": "sha512-UmHoO7TxAEJPIZf8e1Hy6CeFGMeyjqSCpgoBkQZYXFI2JHhzxIyDpr8jVKJJN1dmAePKZ5EX7dC13CmcdTOl7Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.13",
+ "@firebase/logger": "0.4.4",
+ "@firebase/remote-config": "0.6.0",
+ "@firebase/remote-config-types": "0.4.0",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/remote-config-types": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.4.0.tgz",
+ "integrity": "sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/storage": {
+ "version": "0.13.7",
+ "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.13.7.tgz",
+ "integrity": "sha512-FkRyc24rK+Y6EaQ1tYFm3TevBnnfSNA0VyTfew2hrYyL/aYfatBg7HOgktUdB4kWMHNA9VoTotzZTGoLuK92wg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.13",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/storage-compat": {
+ "version": "0.3.17",
+ "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.17.tgz",
+ "integrity": "sha512-CBlODWEZ5b6MJWVh21VZioxwxNwVfPA9CAdsk+ZgVocJQQbE2oDW1XJoRcgthRY1HOitgbn4cVrM+NlQtuUYhw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.13",
+ "@firebase/storage": "0.13.7",
+ "@firebase/storage-types": "0.8.3",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/storage-types": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz",
+ "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@firebase/app-types": "0.x",
+ "@firebase/util": "1.x"
+ }
+ },
+ "node_modules/@firebase/util": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.11.0.tgz",
+ "integrity": "sha512-PzSrhIr++KI6y4P6C/IdgBNMkEx0Ex6554/cYd0Hm+ovyFSJtJXqb/3OSIdnBoa2cpwZT1/GW56EmRc5qEc5fQ==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@firebase/vertexai": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@firebase/vertexai/-/vertexai-1.2.1.tgz",
+ "integrity": "sha512-cukZ5ne2RsOWB4PB1EO6nTXgOLxPMKDJfEn+XnSV5ZKWM0ID5o0DvbyS59XihFaBzmy2SwJldP5ap7/xUnW4jA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/app-check-interop-types": "0.3.3",
+ "@firebase/component": "0.6.13",
+ "@firebase/logger": "0.4.4",
+ "@firebase/util": "1.11.0",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x",
+ "@firebase/app-types": "0.x"
+ }
+ },
+ "node_modules/@firebase/webchannel-wrapper": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.3.tgz",
+ "integrity": "sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@grpc/grpc-js": {
+ "version": "1.9.15",
+ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz",
+ "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@grpc/proto-loader": "^0.7.8",
+ "@types/node": ">=12.12.47"
+ },
+ "engines": {
+ "node": "^8.13.0 || >=10.10.0"
+ }
+ },
+ "node_modules/@grpc/proto-loader": {
+ "version": "0.7.13",
+ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz",
+ "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "lodash.camelcase": "^4.3.0",
+ "long": "^5.0.0",
+ "protobufjs": "^7.2.5",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@grpc/proto-loader/node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@grpc/proto-loader/node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@grpc/proto-loader/node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -3174,6 +3861,70 @@
}
}
},
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -8403,6 +9154,42 @@
"node": ">=8"
}
},
+ "node_modules/firebase": {
+ "version": "11.6.0",
+ "resolved": "https://registry.npmjs.org/firebase/-/firebase-11.6.0.tgz",
+ "integrity": "sha512-Xqm6j6zszIEmI5nW1MPR8yTafoRTSrW3mWG9Lk9elCJtQDQSiTEkKZiNtUm9y6XfOPl8xoF1TNpxZe8HjgA0Og==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/analytics": "0.10.12",
+ "@firebase/analytics-compat": "0.2.18",
+ "@firebase/app": "0.11.4",
+ "@firebase/app-check": "0.8.13",
+ "@firebase/app-check-compat": "0.3.20",
+ "@firebase/app-compat": "0.2.53",
+ "@firebase/app-types": "0.9.3",
+ "@firebase/auth": "1.10.0",
+ "@firebase/auth-compat": "0.5.20",
+ "@firebase/data-connect": "0.3.3",
+ "@firebase/database": "1.0.14",
+ "@firebase/database-compat": "2.0.5",
+ "@firebase/firestore": "4.7.10",
+ "@firebase/firestore-compat": "0.3.45",
+ "@firebase/functions": "0.12.3",
+ "@firebase/functions-compat": "0.3.20",
+ "@firebase/installations": "0.6.13",
+ "@firebase/installations-compat": "0.2.13",
+ "@firebase/messaging": "0.12.17",
+ "@firebase/messaging-compat": "0.2.17",
+ "@firebase/performance": "0.7.2",
+ "@firebase/performance-compat": "0.2.15",
+ "@firebase/remote-config": "0.6.0",
+ "@firebase/remote-config-compat": "0.2.13",
+ "@firebase/storage": "0.13.7",
+ "@firebase/storage-compat": "0.3.17",
+ "@firebase/util": "1.11.0",
+ "@firebase/vertexai": "1.2.1"
+ }
+ },
"node_modules/flat-cache": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
@@ -11480,6 +12267,12 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
+ "node_modules/lodash.camelcase": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+ "license": "MIT"
+ },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -11510,6 +12303,12 @@
"integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==",
"license": "MIT"
},
+ "node_modules/long": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz",
+ "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==",
+ "license": "Apache-2.0"
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -13991,6 +14790,30 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
+ "node_modules/protobufjs": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.0.tgz",
+ "integrity": "sha512-Z2E/kOY1QjoMlCytmexzYfDm/w5fKAiRwpSzGtdnXW1zC88Z2yXazHHrOtwCzn+7wSxyE8PYM4rvVcMphF9sOA==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/protocol-buffers-schema": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
diff --git a/package.json b/package.json
index 459914d..b68c63f 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^13.5.0",
+ "firebase": "^11.6.0",
"mapbox-gl": "^3.10.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
diff --git a/public/images/bg-landscape-1.jpg b/public/images/bg-landscape-1.jpg
new file mode 100644
index 0000000..cb7a60f
Binary files /dev/null and b/public/images/bg-landscape-1.jpg differ
diff --git a/public/images/bg-landscape-10.jpg b/public/images/bg-landscape-10.jpg
new file mode 100644
index 0000000..6709440
Binary files /dev/null and b/public/images/bg-landscape-10.jpg differ
diff --git a/public/images/bg-landscape-11.jpg b/public/images/bg-landscape-11.jpg
new file mode 100644
index 0000000..967357b
Binary files /dev/null and b/public/images/bg-landscape-11.jpg differ
diff --git a/public/images/bg-landscape-12.jpg b/public/images/bg-landscape-12.jpg
new file mode 100644
index 0000000..644e761
Binary files /dev/null and b/public/images/bg-landscape-12.jpg differ
diff --git a/public/images/bg-landscape-2.jpg b/public/images/bg-landscape-2.jpg
new file mode 100644
index 0000000..4c73906
Binary files /dev/null and b/public/images/bg-landscape-2.jpg differ
diff --git a/public/images/bg-landscape-3.jpg b/public/images/bg-landscape-3.jpg
new file mode 100644
index 0000000..a94586b
Binary files /dev/null and b/public/images/bg-landscape-3.jpg differ
diff --git a/public/images/bg-landscape-4.jpg b/public/images/bg-landscape-4.jpg
new file mode 100644
index 0000000..97fabb4
Binary files /dev/null and b/public/images/bg-landscape-4.jpg differ
diff --git a/public/images/bg-landscape-5.jpg b/public/images/bg-landscape-5.jpg
new file mode 100644
index 0000000..455d528
Binary files /dev/null and b/public/images/bg-landscape-5.jpg differ
diff --git a/public/images/bg-landscape-6.webp b/public/images/bg-landscape-6.webp
new file mode 100644
index 0000000..2cfd0f0
Binary files /dev/null and b/public/images/bg-landscape-6.webp differ
diff --git a/public/images/bg-landscape-7.webp b/public/images/bg-landscape-7.webp
new file mode 100644
index 0000000..ac70efc
Binary files /dev/null and b/public/images/bg-landscape-7.webp differ
diff --git a/public/images/bg-landscape-8.jpg b/public/images/bg-landscape-8.jpg
new file mode 100644
index 0000000..4433fa7
Binary files /dev/null and b/public/images/bg-landscape-8.jpg differ
diff --git a/public/images/bg-landscape-9.jpg b/public/images/bg-landscape-9.jpg
new file mode 100644
index 0000000..a8ce1e0
Binary files /dev/null and b/public/images/bg-landscape-9.jpg differ
diff --git a/src/App.js b/src/App.js
index 3e9eb19..068c16b 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,70 +1,219 @@
-import React from "react";
-import HeatMap from "./MapComponent";
+import React, { useState, useEffect } from "react";
+import HeatMap from "./components/Map/HeatMap";
+import EmailAuth from "./EmailAuth";
+import { auth } from "./firebase";
+import { onAuthStateChanged, signOut } from "firebase/auth";
function App() {
+ const [user, setUser] = useState(null);
+ const [userStats, setUserStats] = useState({
+ articles: 0,
+ likes: 0,
+ dislikes: 0,
+ credibility: 50,
+ });
+
+ useEffect(() => {
+ const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
+ if (firebaseUser) {
+ console.log("โ
Logged in as:", firebaseUser.uid);
+ setUser(firebaseUser);
+
+ const payload = { userid: firebaseUser.uid };
+ console.log("๐ฆ Sending user stats request for:", payload);
+
+ fetch("https://fastapi-service-34404463322.us-central1.run.app/user", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ })
+ .then((res) => res.json())
+ .then((data) => {
+ const formatted = {
+ articles: data.total_articles || 0,
+ likes: data.total_likes_received || 0,
+ dislikes: data.total_fakeflags_received || 0,
+ credibility: data.credibility_score || 50,
+ };
+ console.log("โ
Final formatted userStats:", formatted);
+ setUserStats(formatted);
+ })
+ .catch((err) => {
+ console.error("โ Failed to fetch user stats:", err);
+ setUserStats({
+ articles: 0,
+ likes: 0,
+ dislikes: 0,
+ credibility: 50,
+ });
+ });
+ } else {
+ console.log("๐ User signed out or not authenticated.");
+ setUser(null);
+ setUserStats({
+ articles: 0,
+ likes: 0,
+ dislikes: 0,
+ credibility: 50,
+ });
+ }
+ });
+
+ return () => unsubscribe();
+ }, []);
+
+ const handleSignOut = () => {
+ signOut(auth)
+ .then(() => {
+ console.log("๐ Signed out");
+ setUser(null);
+ setUserStats({
+ articles: 0,
+ likes: 0,
+ dislikes: 0,
+ credibility: 50,
+ });
+ })
+ .catch((error) => console.error("Error signing out:", error));
+ };
+
return (
- {/* Heading Section with Background Image and Compact Overlay */}
+ {user && (
+
+
+
+
+ {user.displayName || user.email}
+
+
+ Sign Out
+
+
+
+ )}
+
-
Global News Map
-
Trending and Popular World Events Visualization
+
Click4News
+
+ Trending and Popular World Events Visualization
+
- {/* Heatmap Component */}
-
+ {user ? (
+
+ ) : (
+
+ )}
);
}
-// Inline CSS for Styling
+// โ
Styles stay unchanged
const styles = {
appContainer: {
display: "flex",
flexDirection: "column",
+ width: "100vw",
+ height: "100vh",
+ overflow: "hidden",
+ margin: 0,
+ padding: 0,
+ },
+ floatingUserInfo: {
+ position: "absolute",
+ top: 10,
+ right: 10,
+ backgroundColor: "rgba(0, 0, 0, 0.75)",
+ color: "white",
+ padding: "10px",
+ borderRadius: "8px",
+ display: "flex",
alignItems: "center",
- backgroundColor: "#f4f4f4",
- minHeight: "100vh",
+ gap: "10px",
+ zIndex: 10,
+ maxWidth: "80%",
+ },
+ avatar: {
+ width: "36px",
+ height: "36px",
+ borderRadius: "50%",
+ objectFit: "cover",
+ },
+ userDetails: {
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "flex-start",
+ },
+ userName: {
+ fontSize: "14px",
+ fontWeight: "bold",
+ color: "#fff",
+ lineHeight: "1.2",
+ maxWidth: "200px",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ },
+ signOutButton: {
+ marginTop: "4px",
+ padding: "4px 8px",
+ backgroundColor: "#f44336",
+ color: "#fff",
+ border: "none",
+ borderRadius: "4px",
+ fontSize: "12px",
+ cursor: "pointer",
},
heroSection: {
+ flex: "0 0 auto",
width: "100%",
textAlign: "center",
- padding: "8px 6px", // Reduced padding to make it more compact
+ padding: "8px 6px",
backgroundImage: "url('/images/news.jpg')",
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
position: "relative",
- color: "white",
- boxShadow: "0px 2px 4px rgba(0, 0, 0, 0.1)",
},
overlay: {
- backgroundColor: "rgba(0, 0, 0, 0.5)", // Slightly reduced opacity
- padding: "10px 15px", // Smaller padding for a more compact look
- borderRadius: "4px",
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
+ padding: "12px 18px",
+ borderRadius: "6px",
display: "inline-block",
},
title: {
- fontSize: "20px", // Reduced font size slightly
+ fontSize: "22px",
fontWeight: "bold",
color: "#ffffff",
- textShadow: "1px 1px 3px rgba(0,0,0,0.6)", // Adjusted shadow for readability
margin: "0",
+ textShadow: "1px 1px 3px rgba(0,0,0,0.6)",
},
subtitle: {
- fontSize: "12px", // Slightly smaller subtitle
+ fontSize: "13px",
fontWeight: "lighter",
color: "#f0f0f0",
- textShadow: "1px 1px 2px rgba(0,0,0,0.4)",
- marginTop: "3px",
+ marginTop: "6px",
+ textShadow: "1px 1px 2px rgba(0,0,0,0.3)",
},
mapContainer: {
+ flex: "1 1 auto",
width: "100%",
- height: "calc(100vh - 80px)", // Reduced height taken up by title bar
+ height: "100%",
+ position: "relative",
+ overflow: "visible",
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
},
};
-export default App;
+export default App;
\ No newline at end of file
diff --git a/src/EmailAuth.js b/src/EmailAuth.js
new file mode 100644
index 0000000..f057430
--- /dev/null
+++ b/src/EmailAuth.js
@@ -0,0 +1,345 @@
+import React, { useState } from "react";
+import { auth, provider } from "./firebase";
+import {
+ signInWithPopup,
+ signInWithEmailAndPassword,
+ createUserWithEmailAndPassword,
+ sendPasswordResetEmail,
+} from "firebase/auth";
+
+const EmailAuth = ({ onSignIn }) => {
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [showPwd, setShowPwd] = useState(false);
+ const [error, setError] = useState("");
+ const [mode, setMode] = useState("login");
+ const [showConfirmPwd, setShowConfirmPwd] = useState(false);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setError("");
+
+ if (mode === "register" && password !== confirmPassword) {
+ return setError("Passwords do not match.");
+ }
+
+ try {
+ const method =
+ mode === "register"
+ ? createUserWithEmailAndPassword
+ : signInWithEmailAndPassword;
+ const result = await method(auth, email, password);
+ onSignIn(result.user);
+ } catch (err) {
+ setError(err.message);
+ }
+ };
+
+ const handleResetPassword = async () => {
+ if (!email) return setError("Enter your email to reset password.");
+ try {
+ await sendPasswordResetEmail(auth, email);
+ alert("Password reset email sent.");
+ } catch (err) {
+ setError(err.message);
+ }
+ };
+
+ const handleGoogleLogin = async () => {
+ try {
+ const result = await signInWithPopup(auth, provider);
+ onSignIn(result.user);
+ } catch (error) {
+ setError(error.message);
+ }
+ };
+
+ return (
+
+
+ {[1, 2, 3].map((i) => (
+
+
+
+ ))}
+ {[4, 5, 6].map((i) => (
+
+
+
+ ))}
+ {[7, 8, 9].map((i) => (
+
+
+
+ ))}
+ {[10, 11, 12].map((i) => (
+
+
+
+ ))}
+
+
+
+
+ );
+};
+
+const styles = {
+ wrapper: {
+ position: "relative",
+ width: "100%",
+ height: "100%",
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ overflow: "hidden",
+ backgroundColor: "#000",
+ },
+ collageContainer: {
+ display: "grid",
+ gridTemplateColumns: "repeat(4, 1fr)",
+ position: "absolute",
+ top: 0,
+ left: 0,
+ width: "100%",
+ height: "100%",
+ zIndex: 0,
+ },
+ sideColumn: {
+ display: "flex",
+ flexDirection: "column",
+ justifyContent: "center",
+ alignItems: "center",
+ gap: "2px",
+ padding: "4px",
+ },
+ collageImage: {
+ width: "100%",
+ height: "auto",
+ maxHeight: "30vh",
+ minHeight: "150px",
+ objectFit: "cover",
+ borderRadius: "8px",
+ opacity: 0.85,
+ boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
+ },
+ container: {
+ position: "relative",
+ zIndex: 1,
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ width: "100%",
+ height: "100%",
+ padding: "40px 20px",
+ },
+ card: {
+ width: "100%",
+ maxWidth: "400px",
+ background: "#fff",
+ borderRadius: "12px",
+ padding: "30px",
+ boxShadow: "0 4px 20px rgba(0,0,0,0.1)",
+ },
+ title: {
+ fontSize: "24px",
+ fontWeight: "600",
+ marginBottom: "8px",
+ },
+ subtitle: {
+ fontSize: "14px",
+ color: "#666",
+ marginBottom: "24px",
+ },
+ input: {
+ width: "100%",
+ padding: "12px",
+ fontSize: "14px",
+ border: "1px solid #ccc",
+ borderRadius: "6px",
+ marginBottom: "16px",
+ boxSizing: "border-box",
+ },
+ passwordField: {
+ position: "relative",
+ marginBottom: "16px",
+ },
+ showBtn: {
+ position: "absolute",
+ right: "12px",
+ top: "12px",
+ fontSize: "13px",
+ color: "#0a66c2",
+ cursor: "pointer",
+ },
+ links: {
+ textAlign: "right",
+ marginBottom: "12px",
+ },
+ link: {
+ color: "#0a66c2",
+ fontSize: "13px",
+ cursor: "pointer",
+ textDecoration: "none",
+ },
+ submitBtn: {
+ width: "100%",
+ padding: "12px",
+ backgroundColor: "#0a66c2",
+ color: "#fff",
+ fontSize: "14px",
+ fontWeight: "bold",
+ border: "none",
+ borderRadius: "6px",
+ cursor: "pointer",
+ marginTop: "16px",
+ },
+ orDivider: {
+ display: "flex",
+ alignItems: "center",
+ margin: "20px 0",
+ },
+ line: {
+ flex: 1,
+ height: "1px",
+ backgroundColor: "#ccc",
+ },
+ or: {
+ margin: "0 10px",
+ color: "#666",
+ fontSize: "12px",
+ },
+ googleBtn: {
+ width: "100%",
+ padding: "10px",
+ fontSize: "14px",
+ borderRadius: "24px",
+ border: "1px solid #000",
+ background: "#fff",
+ color: "#000",
+ cursor: "pointer",
+ fontWeight: "600",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ gap: "8px",
+ },
+ googleIcon: {
+ width: "18px",
+ height: "18px",
+ },
+ footer: {
+ marginTop: "24px",
+ textAlign: "center",
+ fontSize: "14px",
+ color: "#555",
+ },
+ error: {
+ color: "red",
+ fontSize: "13px",
+ marginBottom: "10px",
+ },
+};
+
+export default EmailAuth;
diff --git a/src/GoogleSignIn.js b/src/GoogleSignIn.js
new file mode 100644
index 0000000..aab0b4e
--- /dev/null
+++ b/src/GoogleSignIn.js
@@ -0,0 +1,84 @@
+import React from "react";
+import { auth, provider, signInWithPopup } from "./firebase";
+
+const GoogleSignIn = ({ onSignIn }) => {
+ const handleGoogleLogin = async () => {
+ try {
+ const result = await signInWithPopup(auth, provider);
+ const user = result.user;
+ if (onSignIn) onSignIn(user);
+ } catch (error) {
+ console.error("Google Sign-In error:", error);
+ }
+ };
+
+ return (
+
+
+
๐ Click4News
+
See the worldโs pulse โ visualized on a map.
+
+ Discover trending events, breaking stories, and global insights updated in real-time. Zoom into countries and cities to explore what's buzzing near and far.
+
+
+ Sign in with Google
+
+
+
+ );
+};
+
+const styles = {
+ container: {
+ minHeight: "100vh",
+ width: "100%",
+ background: "linear-gradient(135deg, #1f1c2c, #928dab)",
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ padding: "20px",
+ },
+ glassCard: {
+ backdropFilter: "blur(16px)",
+ WebkitBackdropFilter: "blur(16px)",
+ background: "rgba(255, 255, 255, 0.1)",
+ borderRadius: "16px",
+ padding: "40px 30px",
+ maxWidth: "480px",
+ textAlign: "center",
+ boxShadow: "0 8px 30px rgba(0, 0, 0, 0.3)",
+ border: "1px solid rgba(255, 255, 255, 0.2)",
+ color: "#fff",
+ },
+ heading: {
+ fontSize: "32px",
+ fontWeight: "800",
+ marginBottom: "10px",
+ },
+ tagline: {
+ fontSize: "18px",
+ fontWeight: "500",
+ marginBottom: "18px",
+ color: "#f1f1f1",
+ },
+ description: {
+ fontSize: "15px",
+ lineHeight: "1.6",
+ color: "#ddd",
+ marginBottom: "30px",
+ },
+ signInButton: {
+ padding: "12px 20px",
+ background: "linear-gradient(to right, #ff416c, #ff4b2b)",
+ border: "none",
+ borderRadius: "30px",
+ fontSize: "16px",
+ fontWeight: "bold",
+ color: "#fff",
+ cursor: "pointer",
+ boxShadow: "0 4px 12px rgba(255, 75, 43, 0.4)",
+ transition: "transform 0.2s ease",
+ },
+};
+
+export default GoogleSignIn;
\ No newline at end of file
diff --git a/src/MapComponent.js b/src/MapComponent.js
deleted file mode 100644
index 64da216..0000000
--- a/src/MapComponent.js
+++ /dev/null
@@ -1,295 +0,0 @@
-import React, { Component, createRef } from "react";
-import Map, { Popup, Source, Layer } from "react-map-gl/mapbox";
-import "mapbox-gl/dist/mapbox-gl.css";
-import MapFilters from "./MapFilters";
-
-// ๐ง Load token from .env
-const MAPBOX_ACCESS_TOKEN = process.env.REACT_APP_MAPBOX_ACCESS_TOKEN;
-
-class HeatMap extends Component {
- constructor(props) {
- super(props);
- this.state = {
- sortedNearbyNews: [],
- currentNewsIndex: 0,
- theme: "light",
- zoomLevel: "world",
- viewState: {
- longitude: -105.2705,
- latitude: 40.015,
- zoom: 2,
- },
- geoJsonData: null,
- clickedLocation: null,
- circleRadius: 0,
- showPopup: false,
- selectedNews: null,
- };
- this.mapRef = createRef();
- }
-
- componentDidMount() {
- fetch("https://fastapi-service-34404463322.us-central1.run.app/geojson")
- .then((response) => response.json())
- .then((data) => {
- const jitteredFeatures = this.jitterOverlappingPoints(data.features);
- const jitteredData = {
- type: "FeatureCollection",
- features: jitteredFeatures,
- };
- this.setState({ geoJsonData: jitteredData });
- })
- .catch((error) => console.error("Error loading GeoJSON from API:", error));
- }
-
- jitterOverlappingPoints = (features) => {
- const jittered = [];
- const seenCoords = new Set();
-
- features.forEach((feature) => {
- const [lng, lat] = feature.geometry.coordinates;
- const key = `${lng.toFixed(5)},${lat.toFixed(5)}`;
-
- let jitteredLng = lng;
- let jitteredLat = lat;
-
- if (seenCoords.has(key)) {
- const offsetLng = (Math.random() - 0.5) * 0.02;
- const offsetLat = (Math.random() - 0.5) * 0.02;
- jitteredLng += offsetLng;
- jitteredLat += offsetLat;
- }
-
- seenCoords.add(key);
-
- jittered.push({
- ...feature,
- geometry: {
- ...feature.geometry,
- coordinates: [jitteredLng, jitteredLat],
- },
- });
- });
-
- return jittered;
- };
-
- zoomLevels = {
- world: 2,
- country: 4,
- state: 6,
- city: 10,
- };
-
- handleThemeChange = (theme) => this.setState({ theme });
-
- handleZoomChange = (selectedZoom) => {
- this.setState({ zoomLevel: selectedZoom });
- if (this.mapRef.current) {
- this.mapRef.current.flyTo({
- zoom: this.zoomLevels[selectedZoom],
- essential: true,
- duration: 1000,
- });
- }
- };
-
- handleMapClick = (event) => {
- event.originalEvent.stopPropagation();
- const { lng, lat } = event.lngLat;
- if (!this.state.geoJsonData) return;
-
- // โ
Get all nearby features (within rough radius)
- const nearbyNews = this.state.geoJsonData.features.filter((f) => {
- const [fx, fy] = f.geometry.coordinates;
- const dist = Math.hypot(fx - lng, fy - lat);
- return dist < 3; // adjust as needed (3 is loose distance filter)
- });
-
- if (nearbyNews.length === 0) return;
-
- this.setState({
- clickedLocation: { longitude: lng, latitude: lat },
- sortedNearbyNews: nearbyNews,
- currentNewsIndex: 0,
- selectedNews: nearbyNews[0],
- circleRadius: 5,
- showPopup: false,
- }, () => this.animateCircle(() => this.setState({ showPopup: true })));
- };
-
- animateCircle = (callback) => {
- let radius = 5;
- const interval = setInterval(() => {
- radius += 5;
- this.setState({ circleRadius: radius });
- if (radius > 50) {
- clearInterval(interval);
- if (callback) callback();
- }
- }, 50);
- };
-
- render() {
- const {
- theme, viewState, zoomLevel,
- clickedLocation, circleRadius,
- selectedNews, showPopup, geoJsonData
- } = this.state;
-
- const mapStyles = {
- light: "mapbox://styles/mapbox/outdoors-v12",
- dark: "mapbox://styles/mapbox/dark-v11",
- };
-
- if (!geoJsonData) {
- return Loading map data...
;
- }
-
- return (
-
-
-
-
-
-
-
-
- {clickedLocation && (
-
-
-
- )}
-
- {selectedNews && showPopup && (
-
-
-
this.setState({ selectedNews: null, showPopup: false })}
- style={styles.closeButton}
- >
- โ
-
-
{selectedNews.properties.title}
-
{selectedNews.properties.summary}
-
window.open(selectedNews.properties.link, "_blank")}
- >
- Read More
-
- {this.state.sortedNearbyNews.length > 1 && (
-
{
- const { currentNewsIndex, sortedNearbyNews } = this.state;
- const nextIndex = (currentNewsIndex + 1) % sortedNearbyNews.length;
- this.setState({
- currentNewsIndex: nextIndex,
- selectedNews: sortedNearbyNews[nextIndex],
- });
- }}
- >
- โถ Next News
-
- )}
-
-
- )}
-
-
- );
- }
-}
-
-const styles = {
- popupContainer: {
- width: "300px",
- backgroundColor: "#1a1a1a",
- padding: "15px",
- borderRadius: "8px",
- textAlign: "left",
- color: "#ffffff",
- },
- popupTitle: {
- fontSize: "16px",
- fontWeight: "bold",
- marginBottom: "8px",
- color: "#ffcc00",
- },
- popupSummary: {
- fontSize: "14px",
- lineHeight: "1.5",
- color: "#cccccc",
- marginBottom: "10px",
- },
- readMoreButton: {
- width: "100%",
- padding: "8px",
- backgroundColor: "#ffcc00",
- color: "#000",
- border: "none",
- borderRadius: "4px",
- cursor: "pointer",
- fontSize: "14px",
- fontWeight: "bold",
- },
- closeButton: {
- position: "absolute",
- top: 5,
- right: 10,
- background: "transparent",
- border: "none",
- color: "#fff",
- fontSize: "16px",
- cursor: "pointer"
- }
-};
-
-export default HeatMap;
diff --git a/src/MapFilters.js b/src/MapFilters.js
deleted file mode 100644
index d614fd1..0000000
--- a/src/MapFilters.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import React from "react";
-
-const MapFilters = ({ theme, zoomLevel, onThemeChange, onZoomChange }) => {
- return (
-
- Zoom Level:
- onZoomChange(e.target.value)} value={zoomLevel} style={styles.dropdown}>
- World ๐
- Country ๐ณ๏ธ
- State ๐๏ธ
- City๐
-
-
- Theme:
- onThemeChange(e.target.value)} value={theme} style={styles.dropdown}>
- Light
- Dark
-
-
- );
-};
-
-// Styles for the UI Components
-const styles = {
- filterContainer: {
- position: "absolute",
- top: 10,
- left: 10,
- zIndex: 2,
- backgroundColor: "rgba(255, 255, 255, 0.9)",
- padding: "8px 12px",
- borderRadius: "6px",
- boxShadow: "0px 2px 4px rgba(0, 0, 0, 0.1)",
- display: "flex",
- flexDirection: "column",
- gap: "6px",
- },
- label: {
- fontSize: "13px",
- fontWeight: "bold",
- color: "#333",
- },
- dropdown: {
- backgroundColor: "white",
- color: "#333",
- padding: "4px 8px",
- fontSize: "13px",
- borderRadius: "4px",
- border: "1px solid #ccc",
- cursor: "pointer",
- outline: "none",
- transition: "all 0.2s ease-in-out",
- },
-};
-
-export default MapFilters;
diff --git a/src/components/Features/DisplayControls.js b/src/components/Features/DisplayControls.js
new file mode 100644
index 0000000..5eb5894
--- /dev/null
+++ b/src/components/Features/DisplayControls.js
@@ -0,0 +1,213 @@
+import React, { useState } from "react";
+
+const DisplayControls = ({
+ theme,
+ viewLevel,
+ zoom,
+ onThemeChange,
+ onViewLevelChange,
+ onZoomChange,
+ onResetView,
+}) => {
+ const [isExpanded, setIsExpanded] = useState(true);
+
+ return (
+
+
setIsExpanded(!isExpanded)} style={styles.toggleButton}>
+ {isExpanded ? "๐ฝ Hide Controls" : "๐ผ Show Controls"}
+
+
+ {isExpanded && (
+
+ {/* Theme */}
+
+
Theme
+
+ onThemeChange("light")}
+ >
+ ๐ Light
+
+ onThemeChange("dark")}
+ >
+ ๐ Dark
+
+
+
+
+
+
+ {/* View Level */}
+
+
View Level
+
+ {Object.entries(viewIcons).map(([level, icon]) => (
+
+ onViewLevelChange(level)}
+ />
+ {icon} {level.charAt(0).toUpperCase() + level.slice(1)}
+
+ ))}
+
+
+
+
+
+ {/* Zoom */}
+
+
Zoom: {zoom.toFixed(1)}x
+
+ onZoomChange(parseFloat((zoom - 1).toFixed(1)), "button")}
+ style={styles.zoomButton}
+ >
+ โ
+
+ onZoomChange(parseFloat(e.target.value), "button")}
+ style={styles.slider}
+ />
+ onZoomChange(parseFloat((zoom + 1).toFixed(1)), "button")}
+ style={styles.zoomButton}
+ >
+ โ
+
+
+
+
+
+
+
+
+ โณ Reset to Default View
+
+
+
+ )}
+
+ );
+};
+
+const viewIcons = {
+ world: "๐",
+ country: "๐ณ๏ธ",
+ state: "๐๏ธ",
+ city: "๐",
+};
+
+const styles = {
+ wrapper: {
+ width: "100%", // โ
Fill parent width (260px from HeatMap)
+ position: "relative", // โ
No absolute positioning
+ },
+ toggleButton: {
+ width: "100%",
+ padding: "8px 12px",
+ background: "#5c6bc0",
+ color: "#fff",
+ border: "none",
+ borderRadius: "6px",
+ cursor: "pointer",
+ fontSize: "14px",
+ fontWeight: "bold",
+ },
+ container: {
+ backgroundColor: "#fff",
+ border: "1px solid #ddd",
+ padding: "10px 12px",
+ borderRadius: "8px",
+ marginTop: "8px",
+ display: "flex",
+ flexDirection: "column",
+ gap: "10px", // ๐ฝ change from 14px to 10px
+ },
+ section: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "4px", // ๐ฝ tighter gap between label and control
+ },
+ label: {
+ fontSize: "13px",
+ fontWeight: "bold",
+ color: "#333",
+ },
+ toggleRow: {
+ display: "flex",
+ gap: "8px",
+ },
+ themeButton: {
+ flex: 1,
+ padding: "6px",
+ borderRadius: "6px",
+ fontWeight: "bold",
+ cursor: "pointer",
+ border: "1px solid #ccc",
+ },
+ radioGroup: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "4px",
+ paddingLeft: "4px",
+ },
+ radioLabel: {
+ fontSize: "13px",
+ display: "flex",
+ alignItems: "center",
+ gap: "6px",
+ },
+ zoomControls: {
+ display: "flex",
+ alignItems: "center",
+ gap: "6px",
+ },
+ zoomButton: {
+ padding: "4px 8px",
+ fontSize: "14px",
+ backgroundColor: "#eee",
+ border: "1px solid #ccc",
+ borderRadius: "6px",
+ cursor: "pointer",
+ },
+ slider: {
+ flex: 1,
+ width: "100%", // โ
responsive slider
+ },
+ resetButton: {
+ padding: "6px 10px",
+ backgroundColor: "#eee",
+ color: "#000",
+ border: "1px solid #bbb",
+ borderRadius: "6px",
+ cursor: "pointer",
+ fontSize: "13px",
+ },
+ divider: {
+ height: "1px",
+ border: "none",
+ backgroundColor: "#ccc",
+ margin: "6px 0",
+ },
+};
+
+export default DisplayControls;
diff --git a/src/components/Features/NewsFilters.js b/src/components/Features/NewsFilters.js
new file mode 100644
index 0000000..e344a1a
--- /dev/null
+++ b/src/components/Features/NewsFilters.js
@@ -0,0 +1,226 @@
+import React, { useState } from "react";
+
+const emojiCategories = {
+ AI: "๐ค",
+ Basketball: "๐",
+ Business: "๐ผ",
+ Celebrities: "๐",
+ Conflict: "โ๏ธ",
+ Crime: "๐",
+ Diplomacy: "๐๏ธ",
+ Economy: "๐ฐ",
+ Elections: "๐ณ๏ธ",
+ Energy: "โก",
+ Entertainment: "๐ฌ",
+ Environment: "๐ฟ",
+ Fashion: "๐",
+ Finance: "๐",
+ Food: "๐ฝ๏ธ",
+ Football: "๐",
+ Gaming: "๐ฎ",
+ Government: "๐๏ธ",
+ Health: "๐ฉบ",
+ "Higher Education": "๐",
+ Hurricanes: "๐ช๏ธ",
+ Justice: "โ๏ธ",
+ Law: "๐",
+ Lifestyle: "๐
",
+ Medicine: "๐",
+ "Mental Health": "๐ง ",
+ Military: "๐ช",
+ Music: "๐ต",
+ "Natural Disasters": "๐",
+ Olympics: "๐ฅ",
+ Parenting: "๐จโ๐ฉโ๐ง",
+ Physics: "๐ญ",
+ Police: "๐ฎโโ๏ธ",
+ Politics: "๐๏ธ",
+ Research: "๐",
+ Science: "๐ฌ",
+ Sports: "๐
",
+ Startups: "๐",
+ Sustainability: "๐ฑ",
+ "TV Shows": "๐บ",
+ Technology: "๐ป",
+ Travel: "โ๏ธ",
+ Universities: "๐ซ",
+ "User-Generated": "๐",
+ War: "๐ฃ",
+ Weather: "๐ฆ๏ธ",
+ Wildlife: "๐ฆ",
+ World: "๐",
+};
+
+const NewsFilters = ({ keyword, selectedCategories, onKeywordChange, onCategoryChange }) => {
+ const [isExpanded, setIsExpanded] = useState(true);
+ const [searchText, setSearchText] = useState("");
+
+ const categoryList = Object.keys(emojiCategories).filter((cat) =>
+ cat.toLowerCase().includes(searchText.toLowerCase())
+ );
+
+ const handleCheckboxToggle = (cat) => {
+ let updated = [...selectedCategories];
+ if (updated.includes(cat)) {
+ updated = updated.filter((c) => c !== cat);
+ } else {
+ updated.push(cat);
+ }
+ onCategoryChange(updated);
+ };
+
+ const clearAll = () => onCategoryChange([]);
+
+ return (
+
+
setIsExpanded(!isExpanded)} style={styles.toggleButton}>
+ {isExpanded ? "๐ฝ Hide Filters" : "๐ผ Show Filters"}
+
+
+ {isExpanded && (
+
+ {/* Keyword Input */}
+
Search Articles
+
onKeywordChange(e.target.value)}
+ style={styles.input}
+ />
+
+ {/* Category Search Input */}
+
Filter by Category
+
setSearchText(e.target.value)}
+ style={styles.input}
+ />
+
+ {/* Selected Chips */}
+
+ {selectedCategories.map((cat) => (
+
+ {emojiCategories[cat]} {cat}
+ handleCheckboxToggle(cat)}>
+ ร
+
+
+ ))}
+
+
+ {/* Checkbox List */}
+
+ {categoryList.map((cat) => (
+
+ handleCheckboxToggle(cat)}
+ />
+ {emojiCategories[cat] || "๐๏ธ"} {cat}
+
+ ))}
+
+
+ {/* Clear All Button (Moved Below) */}
+
+ โ Clear All
+
+
+ )}
+
+ );
+};
+
+const styles = {
+ wrapper: {
+ width: "100%",
+ position: "relative",
+ },
+ toggleButton: {
+ width: "100%",
+ padding: "8px 12px",
+ background: "#5c6bc0",
+ color: "#fff",
+ border: "none",
+ borderRadius: "6px",
+ cursor: "pointer",
+ fontSize: "14px",
+ fontWeight: "bold",
+ boxShadow: "0px 1px 3px rgba(0, 0, 0, 0.15)",
+ },
+ container: {
+ backgroundColor: "#fff",
+ border: "1px solid #ddd",
+ padding: "10px 12px",
+ borderRadius: "8px",
+ marginTop: "8px",
+ display: "flex",
+ flexDirection: "column",
+ gap: "10px",
+ },
+ label: {
+ fontSize: "13px",
+ fontWeight: "bold",
+ color: "#333",
+ marginBottom: "2px",
+ },
+ input: {
+ width: "100%",
+ padding: "6px 10px",
+ borderRadius: "6px",
+ border: "1px solid #ccc",
+ fontSize: "13px",
+ boxSizing: "border-box",
+ },
+ chipContainer: {
+ display: "flex",
+ flexWrap: "wrap",
+ gap: "4px",
+ marginBottom: "2px",
+ },
+ chip: {
+ display: "flex",
+ alignItems: "center",
+ backgroundColor: "#e0e0e0",
+ borderRadius: "16px",
+ padding: "4px 8px",
+ fontSize: "12px",
+ color: "#333",
+ },
+ chipClose: {
+ marginLeft: "6px",
+ cursor: "pointer",
+ fontWeight: "bold",
+ },
+ checkboxList: {
+ maxHeight: "140px",
+ overflowY: "auto",
+ border: "1px solid #ddd",
+ borderRadius: "6px",
+ padding: "4px",
+ backgroundColor: "#f9f9f9",
+ },
+ checkboxLabel: {
+ display: "block",
+ marginBottom: "4px",
+ fontSize: "13px",
+ color: "#333",
+ },
+ clearButton: {
+ backgroundColor: "#eee",
+ color: "#000",
+ fontSize: "13px",
+ fontWeight: "bold",
+ border: "1px solid #bbb",
+ borderRadius: "6px",
+ padding: "6px 10px",
+ cursor: "pointer",
+ width: "100%",
+ },
+};
+
+export default NewsFilters;
diff --git a/src/components/Map/HeatLayers.js b/src/components/Map/HeatLayers.js
new file mode 100644
index 0000000..84669ad
--- /dev/null
+++ b/src/components/Map/HeatLayers.js
@@ -0,0 +1,59 @@
+import React from "react";
+import { Source, Layer } from "react-map-gl/mapbox";
+
+const HeatLayers = ({ filteredData, clickedLocation, circleRadius }) => {
+ const emptyGeoJSON = {
+ type: "FeatureCollection",
+ features: [],
+ };
+
+ return (
+ <>
+ {/* ๐ฅ Heatmap Layer */}
+
+
+
+
+ {/* ๐ Click Circle Animation Layer */}
+ {clickedLocation && (
+
+
+
+ )}
+ >
+ );
+};
+
+export default HeatLayers;
diff --git a/src/components/Map/HeatMap.js b/src/components/Map/HeatMap.js
new file mode 100644
index 0000000..c98cbb9
--- /dev/null
+++ b/src/components/Map/HeatMap.js
@@ -0,0 +1,291 @@
+import React, { Component, createRef } from "react";
+import Map, { Popup } from "react-map-gl/mapbox";
+import "mapbox-gl/dist/mapbox-gl.css";
+
+import DisplayControls from "../Features/DisplayControls";
+import NewsFilters from "../Features/NewsFilters";
+import HeatLayers from "./HeatLayers";
+import PopupCard from "./PopupCard";
+import UserDetailsPanel from "../User/UserStatsPanel";
+import AddNewsButton from "../User/AddNewsButton";
+
+const MAPBOX_ACCESS_TOKEN = process.env.REACT_APP_MAPBOX_ACCESS_TOKEN;
+
+const viewZoomMap = {
+ world: 2,
+ country: 4,
+ state: 6,
+ city: 10,
+};
+
+class HeatMap extends Component {
+ constructor(props) {
+ super(props);
+ this.mapRef = createRef();
+ this.circleInterval = null;
+ this.state = {
+ theme: "light",
+ viewLevel: "world",
+ zoom: viewZoomMap.world,
+ viewState: {
+ longitude: -105.0,
+ latitude: 39.7392,
+ zoom: viewZoomMap.world,
+ },
+ geoJsonData: null,
+ clickedLocation: null,
+ sortedNearbyNews: [],
+ currentNewsIndex: 0,
+ showPopup: false,
+ circleRadius: 0,
+ keyword: "",
+ selectedCategories: [],
+ };
+ }
+ componentDidUpdate(prevProps) {
+ if (prevProps.userStats !== this.props.userStats) {
+ console.log("๐ User stats updated in HeatMap:", this.props.userStats);
+
+ }
+ }
+ componentDidMount() {
+ fetch("https://fastapi-service-34404463322.us-central1.run.app/geojson")
+ .then((res) => res.json())
+ .then((data) => {
+ const jittered = this.jitterOverlappingPoints(data.features);
+ this.setState({
+ geoJsonData: {
+ type: "FeatureCollection",
+ features: jittered,
+ },
+ });
+ });
+ }
+
+ jitterOverlappingPoints = (features) => {
+ const seen = new Set();
+ return features.map((f) => {
+ let [lng, lat] = f.geometry.coordinates;
+ const key = `${lng.toFixed(4)}|${lat.toFixed(4)}`;
+ if (seen.has(key)) {
+ lng += (Math.random() - 0.5) * 0.01;
+ lat += (Math.random() - 0.5) * 0.01;
+ }
+ seen.add(key);
+ return { ...f, geometry: { ...f.geometry, coordinates: [lng, lat] } };
+ });
+ };
+
+ handleThemeChange = (theme) => this.setState({ theme });
+
+ handleViewLevelChange = (viewLevel) => {
+ const zoom = viewZoomMap[viewLevel];
+ this.setState((prev) => ({
+ viewLevel,
+ zoom,
+ viewState: { ...prev.viewState, zoom },
+ }));
+ if (this.mapRef.current) {
+ this.mapRef.current.flyTo({ zoom, duration: 300 });
+ }
+ };
+
+ handleZoomChange = (zoom, source = "manual") => {
+ const matched = Object.entries(viewZoomMap).find(([_, z]) => Math.abs(z - zoom) < 0.6);
+ const newLevel = matched ? matched[0] : this.state.viewLevel;
+
+ this.setState((prev) => ({
+ zoom,
+ viewLevel: newLevel,
+ viewState: { ...prev.viewState, zoom },
+ }));
+
+ if (source === "button" && this.mapRef.current) {
+ this.mapRef.current.flyTo({ zoom, duration: 300 });
+ }
+ };
+
+ handleKeywordChange = (keyword) => this.setState({ keyword });
+ handleCategoryChange = (selectedCategories) => this.setState({ selectedCategories });
+
+ handleResetView = () => {
+ const viewState = {
+ longitude: -105.0,
+ latitude: 39.7392,
+ zoom: viewZoomMap.world,
+ };
+ this.setState({
+ viewLevel: "world",
+ zoom: viewZoomMap.world,
+ viewState,
+ });
+ if (this.mapRef.current) {
+ this.mapRef.current.flyTo({ center: [-105.0, 39.7392], zoom: viewZoomMap.world, duration: 300 });
+ }
+ };
+
+ getFilteredFeatures = () => {
+ const { geoJsonData, keyword, selectedCategories } = this.state;
+ if (!geoJsonData) return [];
+
+ return geoJsonData.features.filter((f) => {
+ const combinedText = `${f.properties.title || ""} ${f.properties.summary || ""} ${f.properties.category || ""}`;
+ const textMatch = combinedText.toLowerCase().includes(keyword.toLowerCase());
+ const catMatch = selectedCategories.length === 0 || selectedCategories.includes(f.properties.category);
+ return textMatch && catMatch;
+ });
+ };
+
+ handleMapClick = (event) => {
+ const { lng, lat } = event.lngLat;
+ const filteredFeatures = this.getFilteredFeatures();
+ if (!filteredFeatures || !this.mapRef.current) return;
+
+ const map = this.mapRef.current.getMap();
+ const clickedPixel = map.project([lng, lat]);
+ const thresholdPixels = 30;
+
+ const nearbyNews = filteredFeatures.filter((f) => {
+ const screenCoords = map.project(f.geometry.coordinates);
+ const dx = screenCoords.x - clickedPixel.x;
+ const dy = screenCoords.y - clickedPixel.y;
+ const dist = Math.sqrt(dx * dx + dy * dy);
+ return dist < thresholdPixels;
+ });
+
+ if (nearbyNews.length === 0) return;
+
+ this.setState({
+ clickedLocation: { longitude: lng, latitude: lat },
+ sortedNearbyNews: nearbyNews,
+ currentNewsIndex: 0,
+ circleRadius: 10,
+ });
+
+ if (this.circleInterval) clearInterval(this.circleInterval);
+ let radius = 10;
+ this.circleInterval = setInterval(() => {
+ radius += 3;
+ if (radius >= 50) {
+ clearInterval(this.circleInterval);
+ this.setState({ circleRadius: 50 ,showPopup: true });
+ } else {
+ this.setState({ circleRadius: radius });
+ }
+ }, 30);
+ };
+
+ render() {
+ //console.log("๐ฐ Received userStats in HeatMap:", this.props.userStats);
+ const {
+ theme, viewLevel, zoom, viewState,
+ clickedLocation, sortedNearbyNews, currentNewsIndex,
+ showPopup, circleRadius, keyword, selectedCategories
+ } = this.state;
+
+ const selectedNews = sortedNearbyNews[currentNewsIndex];
+ const filteredData = {
+ type: "FeatureCollection",
+ features: this.getFilteredFeatures(),
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
{
+ this.handleZoomChange(evt.viewState.zoom, "scroll");
+ }}
+ mapStyle={{
+ light: "mapbox://styles/mapbox/outdoors-v12",
+ dark: "mapbox://styles/mapbox/dark-v11",
+ }[theme]}
+ style={{ width: "100%", height: "100%" }}
+ >
+
+
+ {selectedNews && showPopup && (
+
+ 1}
+ onClose={() =>
+ this.setState({
+ showPopup: false,
+ clickedLocation: null,
+ circleRadius: 0,
+ })
+ }
+ onNext={() => {
+ const nextIndex = (currentNewsIndex + 1) % sortedNearbyNews.length;
+ this.setState({ currentNewsIndex: nextIndex });
+ }}
+ />
+
+ )}
+
+
+
+
+ );
+ }
+}
+
+const styles = {
+ controlsWrapper: {
+ position: "absolute",
+ top: "12px",
+ left: "10px",
+ zIndex: 2,
+ display: "flex",
+ flexDirection: "column",
+ gap: "12px",
+ width: "260px",
+ },
+ rightPanel: {
+ position: "absolute",
+ top: "12px",
+ right: "10px",
+ zIndex: 2,
+ width: "260px",
+ display: "flex",
+ flexDirection: "column",
+ gap: "12px",
+ },
+};
+
+export default HeatMap;
\ No newline at end of file
diff --git a/src/components/Map/PopupCard.js b/src/components/Map/PopupCard.js
new file mode 100644
index 0000000..ccd3957
--- /dev/null
+++ b/src/components/Map/PopupCard.js
@@ -0,0 +1,195 @@
+import React, { useState, useEffect } from "react";
+import styles from "../../styles";
+import { getAuth } from "firebase/auth";
+
+const PopupCard = ({ selectedNews, multipleNews, onClose, onNext }) => {
+ const [userVote, setUserVote] = useState(null); // "like" | "fake" | null
+ const [likes, setLikes] = useState(0);
+ const [fakeFlags, setFakeFlags] = useState(0);
+ const [trust, setTrust] = useState({
+ label: "Community Voice",
+ color: "#fbc02d",
+ emoji: "๐ฃ",
+ });
+
+ // Function to send vote to the backend
+ const sendVoteToBackend = async (type) => {
+ const user = getAuth().currentUser;
+ if (!user) return;
+
+ const payload = {
+ type,
+ message_id: selectedNews.properties.message_id,
+ userid: user.uid,
+ };
+
+ try {
+ await fetch("https://sqs-backend-573766487049.us-central1.run.app/user_news", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ });
+ console.log("Vote sent:", payload);
+ } catch (error) {
+ console.error("Failed to send vote:", error);
+ }
+ };
+
+ // Function to determine trust tier based on category and random values for other categories
+ const determineTrustTier = (category = "") => {
+ if (category === "User-Generated") {
+ // If category is User-Generated, set trust to "Community Voice"
+ return { label: "Community Voice", color: "#fbc02d", emoji: "๐ฃ" };
+ }
+
+ // Generate a random credibility score between 61 and 100
+ const randomCred = Math.random() * (100 - 61) + 61;
+
+ // Set trust level based on the random credibility score
+ if (randomCred >= 81)
+ return { label: "Verified Source", color: "#2e7d32", emoji: "๐ก" };
+ return { label: "Trusted Contributor", color: "#43a047", emoji: "โ
" };
+ };
+
+ // Fetch credibility and update trust tier on component mount
+ useEffect(() => {
+ setUserVote(null);
+ setLikes(selectedNews.properties.likes || 0);
+ setFakeFlags(selectedNews.properties.fakeflags || 0);
+
+ const category = selectedNews.properties.category;
+
+ // Set trust tier based on category
+ const tier = determineTrustTier(category);
+ setTrust(tier);
+ }, [selectedNews]);
+
+ // Handle user likes
+ const handleLike = async () => {
+ if (userVote === "like") {
+ setLikes((prev) => prev - 1);
+ setUserVote(null);
+ } else {
+ if (userVote === "fake") setFakeFlags((prev) => prev - 1);
+ setLikes((prev) => prev + 1);
+ setUserVote("like");
+ }
+ await sendVoteToBackend("LIKED");
+ };
+
+ // Handle fake flag votes
+ const handleFakeFlag = async () => {
+ if (userVote === "fake") {
+ setFakeFlags((prev) => prev - 1);
+ setUserVote(null);
+ } else {
+ if (userVote === "like") setLikes((prev) => prev - 1);
+ setFakeFlags((prev) => prev + 1);
+ setUserVote("fake");
+ }
+ await sendVoteToBackend("FAKEFLAGGED");
+ };
+
+ // Convert timestamp to a readable date format
+ const publishedDate = new Date(
+ selectedNews.properties.timestamp * 1000
+ ).toLocaleString(undefined, {
+ dateStyle: "medium",
+ timeStyle: "short",
+ });
+
+ // Set source value based on category
+ const source =
+ selectedNews.properties.category === "User-Generated"
+ ? "User-Generated"
+ : selectedNews.properties.userid || "Unknown";
+
+ // Highlight if the source is User-Generated
+ const sourceStyle =
+ selectedNews.properties.category === "User-Generated"
+ ? { backgroundColor: "#ffc107", color: "#000", fontWeight: "bold" }
+ : {};
+
+ return (
+
+
+ โ
+
+
+
{selectedNews.properties.title}
+
{selectedNews.properties.summary}
+
+ {/* Source, Published Date, and Trust Level */}
+
+
+ Source: {" "}
+ {source}
+
+
+ Published: {publishedDate}
+
+
+ {trust.emoji} {trust.label}
+
+
+
+
window.open(selectedNews.properties.link, "_blank")}
+ >
+ Read More
+
+
+
+
+ ๐ Likes {likes}
+
+
+
+ ๐ฉ Fake Flags {fakeFlags}
+
+
+
+ {multipleNews && (
+
+ โถ Next News
+
+ )}
+
+ );
+};
+
+export default PopupCard;
diff --git a/src/components/User/AddNewsButton.js b/src/components/User/AddNewsButton.js
new file mode 100644
index 0000000..b8f2b94
--- /dev/null
+++ b/src/components/User/AddNewsButton.js
@@ -0,0 +1,43 @@
+import React, { useState } from "react";
+import AddNewsModal from "./AddNewsModal";
+
+const AddNewsButton = () => {
+ const [showModal, setShowModal] = useState(false);
+
+ return (
+ <>
+ setShowModal(true)}
+ onMouseEnter={(e) => (e.currentTarget.style.opacity = "0.9")}
+ onMouseLeave={(e) => (e.currentTarget.style.opacity = "1")}
+ >
+ ๐ Add News
+
+
+ {showModal && setShowModal(false)} />}
+ >
+ );
+};
+
+const styles = {
+ fab: {
+ position: "fixed",
+ bottom: "80px",
+ right: "10px",
+ width: "260px",
+ backgroundColor: "#e53935",
+ color: "#fff",
+ border: "none",
+ borderRadius: "10px",
+ padding: "14px 0",
+ fontSize: "15px",
+ fontWeight: "bold",
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.3)",
+ cursor: "pointer",
+ zIndex: 1000,
+ transition: "all 0.2s ease",
+ },
+};
+
+export default AddNewsButton;
diff --git a/src/components/User/AddNewsModal.js b/src/components/User/AddNewsModal.js
new file mode 100644
index 0000000..2f309b8
--- /dev/null
+++ b/src/components/User/AddNewsModal.js
@@ -0,0 +1,224 @@
+import React, { useState } from "react";
+import { getAuth } from "firebase/auth";
+import { v4 as uuidv4 } from "uuid";
+
+const AddNewsModal = ({ onClose }) => {
+ const [title, setTitle] = useState("");
+ const [summary, setSummary] = useState("");
+ const [link, setLink] = useState("");
+
+ const handleSubmit = () => {
+ if (!title.trim() || !summary.trim()) {
+ alert("Title and Summary are required.");
+ return;
+ }
+
+ if (!navigator.geolocation) {
+ alert("Geolocation not supported.");
+ return;
+ }
+
+ navigator.geolocation.getCurrentPosition(
+ async (position) => {
+ const now = new Date();
+ const isoString = now.toISOString();
+ const [date, time] = isoString.split("T");
+ const formattedTime = time.split(".")[0];
+ const lat = position.coords.latitude;
+ const lng = position.coords.longitude;
+
+ // If link is empty, assign a default link
+ const finalLink = link.trim() === "" ? "https://www.google.com/" : link;
+
+ const payload = {
+ uri: uuidv4(),
+ lang: "eng",
+ isDuplicate: false,
+ date,
+ time: formattedTime,
+ dateTime: isoString,
+ dateTimePub: isoString,
+ dataType: "news",
+ sim: 0,
+ url: finalLink, // Use the final link (either provided or default)
+ title,
+ body: summary,
+ userid: getAuth().currentUser?.uid || "anonymous",
+ coordinates: { lat, lng },
+ geoJson: {
+ type: "Location",
+ geometry: { type: "Point", coordinates: [lng, lat] },
+ properties: { name: "Unknown" },
+ },
+ category: "User-Generated",
+ };
+
+ try {
+ await fetch("https://sqs-backend-573766487049.us-central1.run.app/user_news", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ });
+
+ setTimeout(() => {
+ onClose();
+ }, 50);
+
+ setTimeout(() => {
+ alert("โ
News Submitted!");
+ }, 150);
+ } catch (err) {
+ alert("Failed to submit news.");
+ console.error(err);
+ }
+ },
+ (error) => {
+ alert("Location permission is required to submit.");
+ console.error(error);
+ }
+ );
+ };
+
+ return (
+
+
+
๐ Submit News
+
+
+ Title *
+ setTitle(e.target.value)}
+ placeholder="Headline for the article"
+ style={styles.input}
+ />
+
+
+
+ Summary *
+
+
+
+ Link (optional)
+ setLink(e.target.value)}
+ placeholder="https://example.com"
+ style={styles.input}
+ />
+
+
+
+
+ โ Cancel
+
+
+ ๐ Submit
+
+
+
+
+ );
+};
+
+const styles = {
+ backdrop: {
+ position: "fixed",
+ top: 0,
+ left: 0,
+ width: "100vw",
+ height: "100vh",
+ backgroundColor: "rgba(0,0,0,0.6)",
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ zIndex: 9999,
+ backdropFilter: "blur(3px)",
+ },
+ modal: {
+ backgroundColor: "#1f1f1f",
+ color: "#eee",
+ padding: "24px 26px",
+ borderRadius: "12px",
+ width: "400px",
+ boxShadow: "0 6px 18px rgba(0,0,0,0.4)",
+ display: "flex",
+ flexDirection: "column",
+ gap: "16px",
+ fontFamily: "'Segoe UI', sans-serif",
+ },
+ header: {
+ margin: 0,
+ fontSize: "20px",
+ fontWeight: "bold",
+ color: "#ffcc00",
+ },
+ formGroup: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "6px",
+ },
+ label: {
+ fontSize: "13px",
+ fontWeight: "bold",
+ color: "#ccc",
+ },
+ input: {
+ padding: "10px 12px",
+ borderRadius: "6px",
+ border: "1px solid #444",
+ backgroundColor: "#2c2c2c",
+ color: "#fff",
+ fontSize: "14px",
+ outline: "none",
+ },
+ textarea: {
+ minHeight: "80px",
+ resize: "vertical",
+ padding: "10px 12px",
+ borderRadius: "6px",
+ border: "1px solid #444",
+ backgroundColor: "#2c2c2c",
+ color: "#fff",
+ fontSize: "14px",
+ outline: "none",
+ },
+ buttonRow: {
+ display: "flex",
+ justifyContent: "space-between",
+ gap: "12px",
+ marginTop: "10px",
+ },
+ submit: {
+ flex: 1,
+ padding: "10px 14px",
+ borderRadius: "6px",
+ border: "none",
+ backgroundColor: "#4caf50",
+ color: "#fff",
+ fontWeight: "bold",
+ cursor: "pointer",
+ transition: "background 0.2s",
+ },
+ cancel: {
+ flex: 1,
+ padding: "10px 14px",
+ borderRadius: "6px",
+ border: "none",
+ backgroundColor: "#f44336",
+ color: "#fff",
+ fontWeight: "bold",
+ cursor: "pointer",
+ transition: "background 0.2s",
+ },
+};
+
+export default AddNewsModal;
diff --git a/src/components/User/UserStatsPanel.js b/src/components/User/UserStatsPanel.js
new file mode 100644
index 0000000..91b0f6a
--- /dev/null
+++ b/src/components/User/UserStatsPanel.js
@@ -0,0 +1,207 @@
+import React, {useEffect, useState} from "react";
+
+const UserStatsPanel = ({ userStats = {} }) => {
+ console.log("๐งฉ Stats received in UserStatsPanel:", userStats);
+ const {
+ articles = 0,
+ likes = 0,
+ dislikes = 0,
+ credibility = 50,
+ } = userStats;
+ useEffect(() => {
+ console.log(" Updated userStats prop:", userStats);
+ }, [userStats]);
+ const [isExpanded, setIsExpanded] = useState(true);
+
+ const trustTier = (cred) => {
+ if (cred >= 81) return { label: "Verified Source", icon: "๐ก", color: "#2e7d32" };
+ if (cred >= 61) return { label: "Trusted Contributor", icon: "โ
", color: "#43a047" };
+ if (cred >= 41) return { label: "Community Voice", icon: "๐ฃ", color: "#fbc02d" };
+ if (cred >= 21) return { label: "Unreliable Source", icon: "โ", color: "#fb8c00" };
+ return { label: "Flagged Account", icon: "๐ฉ", color: "#e53935" };
+ };
+
+ const tierData = [
+ { min: 0, max: 20, emoji: "๐ฉ", range: "0โ20", label: "Flagged Account" },
+ { min: 21, max: 40, emoji: "โ", range: "21โ40", label: "Unreliable Source" },
+ { min: 41, max: 60, emoji: "๐ฃ", range: "41โ60", label: "Community Voice" },
+ { min: 61, max: 80, emoji: "โ
", range: "61โ80", label: "Trusted Contributor" },
+ { min: 81, max: 100, emoji: "๐ก", range: "81โ100", label: "Verified Source" },
+ ];
+
+ const { label: tierLabel, icon, color } = trustTier(credibility);
+
+ return (
+
+
setIsExpanded(!isExpanded)} style={styles.toggleButton}>
+ {isExpanded ? "๐ฝ User Stats" : "๐ผ User Stats"}
+
+
+ {isExpanded && (
+
+
+
Trust Level
+
{icon} {tierLabel}
+
+
+
+
+
+
Trust Score
+
+
+
{Math.round(credibility)} points
+
+
+
+
+
๐ Articles Submitted: {articles}
+
๐ Total Likes: {likes}
+
๐ฉ Total Fake Flags: {dislikes}
+
+
+
+ {tierData.map((row, idx) => (
+
= row.min && credibility <= row.max ? "bold" : "normal",
+ backgroundColor: credibility >= row.min && credibility <= row.max ? "#eef4ff" : "transparent",
+ border: "1px solid #eee",
+ }}
+ >
+ {row.emoji}
+ {row.range}
+ {row.label}
+
+ ))}
+
+
+ )}
+
+ );
+};
+
+const styles = {
+ wrapper: {
+ width: "100%",
+ position: "relative",
+ },
+ toggleButton: {
+ width: "100%",
+ padding: "8px 12px",
+ background: "#5c6bc0",
+ color: "#fff",
+ border: "none",
+ borderRadius: "6px",
+ cursor: "pointer",
+ fontSize: "14px",
+ fontWeight: "bold",
+ },
+ container: {
+ backgroundColor: "#fff",
+ border: "1px solid #ddd",
+ padding: "10px 12px",
+ borderRadius: "8px",
+ marginTop: "8px",
+ display: "flex",
+ flexDirection: "column",
+ gap: "14px",
+ },
+ section: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "6px",
+ },
+ sectionTable: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "12px",
+ marginTop: "10px",
+ },
+ tableRow: {
+ display: "flex",
+ justifyContent: "space-between",
+ alignItems: "center",
+ fontSize: "14px",
+ },
+ tableLabel: {
+ fontWeight: "500",
+ color: "#555",
+ },
+ tableValue: {
+ fontWeight: "bold",
+ color: "#333",
+ },
+ label: {
+ fontSize: "13px",
+ fontWeight: "bold",
+ color: "#222",
+ },
+ tierDisplay: {
+ fontSize: "15px",
+ fontWeight: "bold",
+ },
+ trustBarWrapper: {
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ },
+ trustBar: {
+ width: "100%",
+ height: "14px",
+ background: "linear-gradient(to right, #e53935, #fb8c00, #fdd835, #8bc34a, #43a047)",
+ borderRadius: "8px",
+ position: "relative",
+ marginBottom: "6px",
+ },
+ trustPointer: {
+ position: "absolute",
+ top: "-8px",
+ fontSize: "18px",
+ transition: "left 0.3s ease",
+ },
+ credibilityText: {
+ fontSize: "13px",
+ fontWeight: "bold",
+ color: "#333",
+ },
+ divider: {
+ height: "1px",
+ border: "none",
+ backgroundColor: "#ccc",
+ margin: "6px 0",
+ },
+ tierTable: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "8px",
+ fontSize: "13px",
+ borderTop: "1px solid #ccc",
+ paddingTop: "10px",
+ },
+ tierRow: {
+ display: "flex",
+ justifyContent: "space-between",
+ alignItems: "center",
+ padding: "6px 10px",
+ borderRadius: "6px",
+ fontSize: "13px",
+ },
+ legendCol: {
+ width: "33.33%",
+ textAlign: "center",
+ },
+};
+
+export default UserStatsPanel;
diff --git a/src/firebase.js b/src/firebase.js
new file mode 100644
index 0000000..d35e36f
--- /dev/null
+++ b/src/firebase.js
@@ -0,0 +1,23 @@
+// firebase.js
+import { initializeApp } from "firebase/app";
+import { getAuth, GoogleAuthProvider } from "firebase/auth";
+
+// Load Firebase config from environment variables
+const firebaseConfig = {
+ apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
+ authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
+ projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
+ storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
+ messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
+ appId: process.env.REACT_APP_FIREBASE_APP_ID,
+ measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT_ID
+};
+
+// Initialize Firebase app
+const app = initializeApp(firebaseConfig);
+
+// Setup Auth
+const auth = getAuth(app);
+const provider = new GoogleAuthProvider();
+
+export { auth, provider };
diff --git a/src/index.css b/src/index.css
index 417a3e7..28eced9 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,5 +1,10 @@
-body {
+/* Reset and base styles */
+html, body, #root {
margin: 0;
+ padding: 0;
+ height: 100%;
+ width: 100%;
+ overflow: hidden; /* Prevent both vertical and horizontal scroll */
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
@@ -12,13 +17,9 @@ code {
monospace;
}
-
-/* Force the popup to be centered */
-
-
-/* Ensure the content inside the popup is centered */
+/* Customize Mapbox popup appearance */
.mapboxgl-popup-content {
- background: none !important; /* โ
Remove default white background */
+ background: none !important; /* Remove default white background */
box-shadow: none !important;
border: none !important;
padding: 0px !important;
@@ -27,7 +28,20 @@ code {
align-items: center !important;
}
-/* Hide the default Mapbox popup arrow */
.mapboxgl-popup-tip {
- display: none !important;
+ display: none !important; /* Hide arrow */
+}
+
+/* Ensure map canvas doesn't overflow */
+.mapboxgl-map {
+ width: 100% !important;
+ height: 100% !important;
+ overflow: hidden !important;
+}
+
+.mapboxgl-ctrl-attrib {
+ font-size: 10px !important;
+ opacity: 0.5 !important;
+ bottom: 4px !important;
+ right: 6px !important;
}
\ No newline at end of file
diff --git a/src/styles.js b/src/styles.js
new file mode 100644
index 0000000..aad517e
--- /dev/null
+++ b/src/styles.js
@@ -0,0 +1,106 @@
+const styles = {
+ popupContainer: {
+ width: "300px",
+ backgroundColor: "#1a1a1a",
+ padding: "15px",
+ borderRadius: "8px",
+ textAlign: "left",
+ color: "#ffffff",
+ position: "relative",
+ },
+ popupTitle: {
+ fontSize: "16px",
+ fontWeight: "bold",
+ marginBottom: "8px",
+ color: "#ffcc00",
+ },
+ popupSummary: {
+ fontSize: "14px",
+ lineHeight: "1.5",
+ color: "#cccccc",
+ marginBottom: "10px",
+ },
+ readMoreButton: {
+ width: "100%",
+ padding: "8px",
+ backgroundColor: "#ff4d4f",
+ color: "#fff",
+ border: "none",
+ borderRadius: "4px",
+ cursor: "pointer",
+ fontSize: "14px",
+ fontWeight: "bold",
+ },
+ closeButton: {
+ position: "absolute",
+ top: 5,
+ right: 10,
+ background: "transparent",
+ border: "none",
+ color: "#fff",
+ fontSize: "16px",
+ cursor: "pointer",
+ },
+ voteRow: {
+ display: "flex",
+ justifyContent: "space-between",
+ marginTop: "12px",
+ gap: "8px",
+ },
+ voteButton: {
+ flex: 1,
+ padding: "8px",
+ borderRadius: "6px",
+ fontWeight: "bold",
+ fontSize: "14px",
+ cursor: "pointer",
+ border: "none",
+ },
+
+ // ๐ด Modal Styles for AddNewsModal
+ modalBackdrop: {
+ position: "fixed",
+ top: 0,
+ left: 0,
+ width: "100vw",
+ height: "100vh",
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
+ backdropFilter: "blur(3px)",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ zIndex: 9999,
+ },
+ modalContent: {
+ width: "360px",
+ backgroundColor: "#2c2c2c",
+ borderRadius: "10px",
+ padding: "20px",
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.4)",
+ display: "flex",
+ flexDirection: "column",
+ gap: "12px",
+ color: "#fff",
+ },
+ input: {
+ padding: "8px 12px",
+ fontSize: "14px",
+ borderRadius: "6px",
+ border: "1px solid #888",
+ backgroundColor: "#1e1e1e",
+ color: "#f5f5f5",
+ outline: "none",
+ },
+ clearButton: {
+ padding: "8px 16px",
+ backgroundColor: "#666",
+ color: "#fff",
+ fontSize: "13px",
+ fontWeight: "bold",
+ border: "none",
+ borderRadius: "6px",
+ cursor: "pointer",
+ },
+};
+
+export default styles;