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 +
+ + {user.displayName || user.email} + + +
+
+ )} +
-

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) => ( +
+ {`left-${i}`} +
+ ))} + {[4, 5, 6].map((i) => ( +
+ {`center-left-${i}`} +
+ ))} + {[7, 8, 9].map((i) => ( +
+ {`center-right-${i}`} +
+ ))} + {[10, 11, 12].map((i) => ( +
+ {`right-${i}`} +
+ ))} +
+ +
+
+

{mode === "login" ? "Sign in" : "Join Click4News"}

+

+ {mode === "login" + ? "Stay updated on real-time global stories." + : "Create your account to explore breaking news by location."} +

+ + setEmail(e.target.value)} + style={styles.input} + /> + +
+ setPassword(e.target.value)} + style={{ ...styles.input, marginBottom: 0 }} + /> + setShowPwd(!showPwd)} style={styles.showBtn}> + {showPwd ? "hide" : "show"} + +
+ + {mode === "register" && ( +
+ setConfirmPassword(e.target.value)} + style={{ ...styles.input, marginBottom: 0 }} + /> + setShowConfirmPwd(!showConfirmPwd)} style={styles.showBtn}> + {showConfirmPwd ? "hide" : "show"} + +
+ )} + +
+ {mode === "login" && ( + + Forgot password? + + )} +
+ + {error &&

{error}

} + + + +
+ + or + +
+ + + +

+ {mode === "login" ? "New to Click4News?" : "Already a user?"}{" "} + setMode(mode === "login" ? "register" : "login")} style={styles.link}> + {mode === "login" ? "Join now" : "Sign in"} + +

+
+
+
+ ); +}; + +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. +

+ +
+
+ ); +}; + +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 && ( - -
- -

{selectedNews.properties.title}

-

{selectedNews.properties.summary}

- - {this.state.sortedNearbyNews.length > 1 && ( - - )} -
-
- )} -
-
- ); - } -} - -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 ( -
- - - - - -
- ); -}; - -// 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 ( +
+ + + {isExpanded && ( +
+ {/* Theme */} +
+ +
+ + +
+
+ +
+ + {/* View Level */} +
+ +
+ {Object.entries(viewIcons).map(([level, icon]) => ( + + ))} +
+
+ +
+ + {/* Zoom */} +
+ +
+ + onZoomChange(parseFloat(e.target.value), "button")} + style={styles.slider} + /> + +
+
+ +
+ +
+ +
+
+ )} +
+ ); +}; + +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 ( +
+ + + {isExpanded && ( +
+ {/* Keyword Input */} + + onKeywordChange(e.target.value)} + style={styles.input} + /> + + {/* Category Search Input */} + + setSearchText(e.target.value)} + style={styles.input} + /> + + {/* Selected Chips */} +
+ {selectedCategories.map((cat) => ( +
+ {emojiCategories[cat]} {cat} + handleCheckboxToggle(cat)}> + ร— + +
+ ))} +
+ + {/* Checkbox List */} +
+ {categoryList.map((cat) => ( + + ))} +
+ + {/* Clear All Button (Moved Below) */} + +
+ )} +
+ ); +}; + +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} +
+
+ + + +
+ + + +
+ + {multipleNews && ( + + )} +
+ ); +}; + +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 ( + <> + + + {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

+ +
+ + setTitle(e.target.value)} + placeholder="Headline for the article" + style={styles.input} + /> +
+ +
+ +