diff options
Diffstat (limited to 'site')
| -rw-r--r-- | site/package.json | 1 | ||||
| -rw-r--r-- | site/pnpm-lock.yaml | 837 | ||||
| -rw-r--r-- | site/public/firebase-messaging-sw.js | 143 | ||||
| -rw-r--r-- | site/src/components/NotificationButton.tsx | 196 | ||||
| -rw-r--r-- | site/src/firebase.ts | 81 | ||||
| -rw-r--r-- | site/src/pages/Homepage.tsx | 125 |
6 files changed, 1325 insertions, 58 deletions
diff --git a/site/package.json b/site/package.json index 82723ca..86cc0ef 100644 --- a/site/package.json +++ b/site/package.json @@ -12,6 +12,7 @@ "dependencies": { "@tailwindcss/vite": "^4.1.13", "@vercel/analytics": "^1.5.0", + "firebase": "^12.3.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-router-dom": "^7.9.1", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index c5b7308..cb3d7c4 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -10,10 +10,13 @@ importers: dependencies: '@tailwindcss/vite': specifier: ^4.1.13 - version: 4.1.13(vite@7.1.6(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)) + version: 4.1.13(vite@7.1.6(@types/node@24.6.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)) '@vercel/analytics': specifier: ^1.5.0 version: 1.5.0(react@19.1.1) + firebase: + specifier: ^12.3.0 + version: 12.3.0 react: specifier: ^19.1.1 version: 19.1.1 @@ -41,7 +44,7 @@ importers: version: 19.1.9(@types/react@19.1.13) '@vitejs/plugin-react-swc': specifier: ^4.1.0 - version: 4.1.0(vite@7.1.6(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)) + version: 4.1.0(vite@7.1.6(@types/node@24.6.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)) eslint: specifier: ^9.35.0 version: 9.35.0(jiti@2.5.1) @@ -62,10 +65,10 @@ importers: version: 8.44.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) vite: specifier: ^7.1.6 - version: 7.1.6(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0) + version: 7.1.6(@types/node@24.6.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0) vite-plugin-pwa: specifier: ^1.0.3 - version: 1.0.3(vite@7.1.6(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0))(workbox-build@7.3.0)(workbox-window@7.3.0) + version: 1.0.3(vite@7.1.6(@types/node@24.6.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0))(workbox-build@7.3.0)(workbox-window@7.3.0) packages: @@ -764,6 +767,225 @@ packages: resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@firebase/ai@2.3.0': + resolution: {integrity: sha512-rVZgf4FszXPSFVIeWLE8ruLU2JDmPXw4XgghcC0x/lK9veGJIyu+DvyumjreVhW/RwD3E5cNPWxQunzylhf/6w==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-types': 0.x + + '@firebase/analytics-compat@0.2.24': + resolution: {integrity: sha512-jE+kJnPG86XSqGQGhXXYt1tpTbCTED8OQJ/PQ90SEw14CuxRxx/H+lFbWA1rlFtFSsTCptAJtgyRBwr/f00vsw==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/analytics-types@0.8.3': + resolution: {integrity: sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==} + + '@firebase/analytics@0.10.18': + resolution: {integrity: sha512-iN7IgLvM06iFk8BeFoWqvVpRFW3Z70f+Qe2PfCJ7vPIgLPjHXDE774DhCT5Y2/ZU/ZbXPDPD60x/XPWEoZLNdg==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/app-check-compat@0.4.0': + resolution: {integrity: sha512-UfK2Q8RJNjYM/8MFORltZRG9lJj11k0nW84rrffiKvcJxLf1jf6IEjCIkCamykHE73C6BwqhVfhIBs69GXQV0g==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/app-check-interop-types@0.3.3': + resolution: {integrity: sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==} + + '@firebase/app-check-types@0.5.3': + resolution: {integrity: sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==} + + '@firebase/app-check@0.11.0': + resolution: {integrity: sha512-XAvALQayUMBJo58U/rxW02IhsesaxxfWVmVkauZvGEz3vOAjMEQnzFlyblqkc2iAaO82uJ2ZVyZv9XzPfxjJ6w==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/app-compat@0.5.3': + resolution: {integrity: sha512-rRK9YOvgsAU/+edjgubL1q1FyCMjBZZs+fAWtD36tklawkh6WZV07sNLVSceuni+a21oby6xoad+3R8dfztOrA==} + engines: {node: '>=20.0.0'} + + '@firebase/app-types@0.9.3': + resolution: {integrity: sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==} + + '@firebase/app@0.14.3': + resolution: {integrity: sha512-by1leTfZkwGycPKRWpc+p5/IhpnOj8zaScVi4RRm9fMoFYS3IE87Wzx1Yf/ruVYowXOEuLqYY3VmJw5tU3+0Bg==} + engines: {node: '>=20.0.0'} + + '@firebase/auth-compat@0.6.0': + resolution: {integrity: sha512-J0lGSxXlG/lYVi45wbpPhcWiWUMXevY4fvLZsN1GHh+po7TZVng+figdHBVhFheaiipU8HZyc7ljw1jNojM2nw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/auth-interop-types@0.2.4': + resolution: {integrity: sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==} + + '@firebase/auth-types@0.13.0': + resolution: {integrity: sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/auth@1.11.0': + resolution: {integrity: sha512-5j7+ua93X+IRcJ1oMDTClTo85l7Xe40WSkoJ+shzPrX7OISlVWLdE1mKC57PSD+/LfAbdhJmvKixINBw2ESK6w==} + engines: {node: '>=20.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 + + '@firebase/component@0.7.0': + resolution: {integrity: sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==} + engines: {node: '>=20.0.0'} + + '@firebase/data-connect@0.3.11': + resolution: {integrity: sha512-G258eLzAD6im9Bsw+Qm1Z+P4x0PGNQ45yeUuuqe5M9B1rn0RJvvsQCRHXgE52Z+n9+WX1OJd/crcuunvOGc7Vw==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/database-compat@2.1.0': + resolution: {integrity: sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==} + engines: {node: '>=20.0.0'} + + '@firebase/database-types@1.0.16': + resolution: {integrity: sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==} + + '@firebase/database@1.1.0': + resolution: {integrity: sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==} + engines: {node: '>=20.0.0'} + + '@firebase/firestore-compat@0.4.2': + resolution: {integrity: sha512-cy7ov6SpFBx+PHwFdOOjbI7kH00uNKmIFurAn560WiPCZXy9EMnil1SOG7VF4hHZKdenC+AHtL4r3fNpirpm0w==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/firestore-types@3.0.3': + resolution: {integrity: sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/firestore@4.9.2': + resolution: {integrity: sha512-iuA5+nVr/IV/Thm0Luoqf2mERUvK9g791FZpUJV1ZGXO6RL2/i/WFJUj5ZTVXy5pRjpWYO+ZzPcReNrlilmztA==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/functions-compat@0.4.1': + resolution: {integrity: sha512-AxxUBXKuPrWaVNQ8o1cG1GaCAtXT8a0eaTDfqgS5VsRYLAR0ALcfqDLwo/QyijZj1w8Qf8n3Qrfy/+Im245hOQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/functions-types@0.6.3': + resolution: {integrity: sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==} + + '@firebase/functions@0.13.1': + resolution: {integrity: sha512-sUeWSb0rw5T+6wuV2o9XNmh9yHxjFI9zVGFnjFi+n7drTEWpl7ZTz1nROgGrSu472r+LAaj+2YaSicD4R8wfbw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/installations-compat@0.2.19': + resolution: {integrity: sha512-khfzIY3EI5LePePo7vT19/VEIH1E3iYsHknI/6ek9T8QCozAZshWT9CjlwOzZrKvTHMeNcbpo/VSOSIWDSjWdQ==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/installations-types@0.5.3': + resolution: {integrity: sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==} + peerDependencies: + '@firebase/app-types': 0.x + + '@firebase/installations@0.6.19': + resolution: {integrity: sha512-nGDmiwKLI1lerhwfwSHvMR9RZuIH5/8E3kgUWnVRqqL7kGVSktjLTWEMva7oh5yxQ3zXfIlIwJwMcaM5bK5j8Q==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/logger@0.5.0': + resolution: {integrity: sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==} + engines: {node: '>=20.0.0'} + + '@firebase/messaging-compat@0.2.23': + resolution: {integrity: sha512-SN857v/kBUvlQ9X/UjAqBoQ2FEaL1ZozpnmL1ByTe57iXkmnVVFm9KqAsTfmf+OEwWI4kJJe9NObtN/w22lUgg==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/messaging-interop-types@0.2.3': + resolution: {integrity: sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==} + + '@firebase/messaging@0.12.23': + resolution: {integrity: sha512-cfuzv47XxqW4HH/OcR5rM+AlQd1xL/VhuaeW/wzMW1LFrsFcTn0GND/hak1vkQc2th8UisBcrkVcQAnOnKwYxg==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/performance-compat@0.2.22': + resolution: {integrity: sha512-xLKxaSAl/FVi10wDX/CHIYEUP13jXUjinL+UaNXT9ByIvxII5Ne5150mx6IgM8G6Q3V+sPiw9C8/kygkyHUVxg==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/performance-types@0.2.3': + resolution: {integrity: sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==} + + '@firebase/performance@0.7.9': + resolution: {integrity: sha512-UzybENl1EdM2I1sjYm74xGt/0JzRnU/0VmfMAKo2LSpHJzaj77FCLZXmYQ4oOuE+Pxtt8Wy2BVJEENiZkaZAzQ==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/remote-config-compat@0.2.20': + resolution: {integrity: sha512-P/ULS9vU35EL9maG7xp66uljkZgcPMQOxLj3Zx2F289baTKSInE6+YIkgHEi1TwHoddC/AFePXPpshPlEFkbgg==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/remote-config-types@0.5.0': + resolution: {integrity: sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg==} + + '@firebase/remote-config@0.7.0': + resolution: {integrity: sha512-dX95X6WlW7QlgNd7aaGdjAIZUiQkgWgNS+aKNu4Wv92H1T8Ue/NDUjZHd9xb8fHxLXIHNZeco9/qbZzr500MjQ==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/storage-compat@0.4.0': + resolution: {integrity: sha512-vDzhgGczr1OfcOy285YAPur5pWDEvD67w4thyeCUh6Ys0izN9fNYtA1MJERmNBfqjqu0lg0FM5GLbw0Il21M+g==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/storage-types@0.8.3': + resolution: {integrity: sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/storage@0.14.0': + resolution: {integrity: sha512-xWWbb15o6/pWEw8H01UQ1dC5U3rf8QTAzOChYyCpafV6Xki7KVp3Yaw2nSklUwHEziSWE9KoZJS7iYeyqWnYFA==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/util@1.13.0': + resolution: {integrity: sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==} + engines: {node: '>=20.0.0'} + + '@firebase/webchannel-wrapper@1.0.5': + resolution: {integrity: sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==} + + '@grpc/grpc-js@1.9.15': + resolution: {integrity: sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==} + engines: {node: ^8.13.0 || >=10.10.0} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -815,6 +1037,36 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@rolldown/pluginutils@1.0.0-beta.35': resolution: {integrity: sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg==} @@ -1149,6 +1401,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@24.6.2': + resolution: {integrity: sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==} + '@types/react-dom@19.1.9': resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==} peerDependencies: @@ -1270,6 +1525,10 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -1367,6 +1626,10 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1457,6 +1720,9 @@ packages: electron-to-chromium@1.5.221: resolution: {integrity: sha512-/1hFJ39wkW01ogqSyYoA4goOXOtMRy6B+yvA1u42nnsEGtHzIzmk93aPISumVQeblj47JUHLC9coCjUxb1EvtQ==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + enhanced-resolve@5.18.3: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} @@ -1576,6 +1842,10 @@ packages: fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1600,6 +1870,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + firebase@12.3.0: + resolution: {integrity: sha512-/JVja0IDO8zPETGv4TvvBwo7RwcQFz+RQ3JBETNtUSeqsDdI9G7fhRTkCy1sPKnLzW0xpm/kL8GOj6ncndTT3g==} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -1637,6 +1910,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -1713,6 +1990,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + http-parser-js@0.5.10: + resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} + idb@7.1.1: resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} @@ -1783,6 +2063,10 @@ packages: resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} engines: {node: '>= 0.4'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-generator-function@1.1.0: resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} engines: {node: '>= 0.4'} @@ -1996,6 +2280,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} @@ -2008,6 +2295,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -2148,6 +2438,10 @@ packages: resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} engines: {node: ^14.13.1 || >=16.0.0} + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2217,6 +2511,10 @@ packages: resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} hasBin: true + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2343,6 +2641,10 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + string.prototype.matchall@4.0.12: resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} engines: {node: '>= 0.4'} @@ -2363,6 +2665,10 @@ packages: resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} engines: {node: '>=4'} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + strip-comments@2.0.1: resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} engines: {node: '>=10'} @@ -2420,6 +2726,9 @@ packages: peerDependencies: typescript: '>=4.8.4' + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -2460,6 +2769,9 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + undici-types@7.13.0: + resolution: {integrity: sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==} + unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} @@ -2549,9 +2861,20 @@ packages: yaml: optional: true + web-vitals@4.2.4: + resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -2629,9 +2952,17 @@ packages: workbox-window@7.3.0: resolution: {integrity: sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA==} + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -2639,6 +2970,14 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -3428,6 +3767,336 @@ snapshots: '@eslint/core': 0.15.2 levn: 0.4.1 + '@firebase/ai@2.3.0(@firebase/app-types@0.9.3)(@firebase/app@0.14.3)': + dependencies: + '@firebase/app': 0.14.3 + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/app-types': 0.9.3 + '@firebase/component': 0.7.0 + '@firebase/logger': 0.5.0 + '@firebase/util': 1.13.0 + tslib: 2.8.1 + + '@firebase/analytics-compat@0.2.24(@firebase/app-compat@0.5.3)(@firebase/app@0.14.3)': + dependencies: + '@firebase/analytics': 0.10.18(@firebase/app@0.14.3) + '@firebase/analytics-types': 0.8.3 + '@firebase/app-compat': 0.5.3 + '@firebase/component': 0.7.0 + '@firebase/util': 1.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/analytics-types@0.8.3': {} + + '@firebase/analytics@0.10.18(@firebase/app@0.14.3)': + dependencies: + '@firebase/app': 0.14.3 + '@firebase/component': 0.7.0 + '@firebase/installations': 0.6.19(@firebase/app@0.14.3) + '@firebase/logger': 0.5.0 + '@firebase/util': 1.13.0 + tslib: 2.8.1 + + '@firebase/app-check-compat@0.4.0(@firebase/app-compat@0.5.3)(@firebase/app@0.14.3)': + dependencies: + '@firebase/app-check': 0.11.0(@firebase/app@0.14.3) + '@firebase/app-check-types': 0.5.3 + '@firebase/app-compat': 0.5.3 + '@firebase/component': 0.7.0 + '@firebase/logger': 0.5.0 + '@firebase/util': 1.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/app-check-interop-types@0.3.3': {} + + '@firebase/app-check-types@0.5.3': {} + + '@firebase/app-check@0.11.0(@firebase/app@0.14.3)': + dependencies: + '@firebase/app': 0.14.3 + '@firebase/component': 0.7.0 + '@firebase/logger': 0.5.0 + '@firebase/util': 1.13.0 + tslib: 2.8.1 + + '@firebase/app-compat@0.5.3': + dependencies: + '@firebase/app': 0.14.3 + '@firebase/component': 0.7.0 + '@firebase/logger': 0.5.0 + '@firebase/util': 1.13.0 + tslib: 2.8.1 + + '@firebase/app-types@0.9.3': {} + + '@firebase/app@0.14.3': + dependencies: + '@firebase/component': 0.7.0 + '@firebase/logger': 0.5.0 + '@firebase/util': 1.13.0 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/auth-compat@0.6.0(@firebase/app-compat@0.5.3)(@firebase/app-types@0.9.3)(@firebase/app@0.14.3)': + dependencies: + '@firebase/app-compat': 0.5.3 + '@firebase/auth': 1.11.0(@firebase/app@0.14.3) + '@firebase/auth-types': 0.13.0(@firebase/app-types@0.9.3)(@firebase/util@1.13.0) + '@firebase/component': 0.7.0 + '@firebase/util': 1.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + - '@react-native-async-storage/async-storage' + + '@firebase/auth-interop-types@0.2.4': {} + + '@firebase/auth-types@0.13.0(@firebase/app-types@0.9.3)(@firebase/util@1.13.0)': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.13.0 + + '@firebase/auth@1.11.0(@firebase/app@0.14.3)': + dependencies: + '@firebase/app': 0.14.3 + '@firebase/component': 0.7.0 + '@firebase/logger': 0.5.0 + '@firebase/util': 1.13.0 + tslib: 2.8.1 + + '@firebase/component@0.7.0': + dependencies: + '@firebase/util': 1.13.0 + tslib: 2.8.1 + + '@firebase/data-connect@0.3.11(@firebase/app@0.14.3)': + dependencies: + '@firebase/app': 0.14.3 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.7.0 + '@firebase/logger': 0.5.0 + '@firebase/util': 1.13.0 + tslib: 2.8.1 + + '@firebase/database-compat@2.1.0': + dependencies: + '@firebase/component': 0.7.0 + '@firebase/database': 1.1.0 + '@firebase/database-types': 1.0.16 + '@firebase/logger': 0.5.0 + '@firebase/util': 1.13.0 + tslib: 2.8.1 + + '@firebase/database-types@1.0.16': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.13.0 + + '@firebase/database@1.1.0': + dependencies: + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.7.0 + '@firebase/logger': 0.5.0 + '@firebase/util': 1.13.0 + faye-websocket: 0.11.4 + tslib: 2.8.1 + + '@firebase/firestore-compat@0.4.2(@firebase/app-compat@0.5.3)(@firebase/app-types@0.9.3)(@firebase/app@0.14.3)': + dependencies: + '@firebase/app-compat': 0.5.3 + '@firebase/component': 0.7.0 + '@firebase/firestore': 4.9.2(@firebase/app@0.14.3) + '@firebase/firestore-types': 3.0.3(@firebase/app-types@0.9.3)(@firebase/util@1.13.0) + '@firebase/util': 1.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/firestore-types@3.0.3(@firebase/app-types@0.9.3)(@firebase/util@1.13.0)': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.13.0 + + '@firebase/firestore@4.9.2(@firebase/app@0.14.3)': + dependencies: + '@firebase/app': 0.14.3 + '@firebase/component': 0.7.0 + '@firebase/logger': 0.5.0 + '@firebase/util': 1.13.0 + '@firebase/webchannel-wrapper': 1.0.5 + '@grpc/grpc-js': 1.9.15 + '@grpc/proto-loader': 0.7.15 + tslib: 2.8.1 + + '@firebase/functions-compat@0.4.1(@firebase/app-compat@0.5.3)(@firebase/app@0.14.3)': + dependencies: + '@firebase/app-compat': 0.5.3 + '@firebase/component': 0.7.0 + '@firebase/functions': 0.13.1(@firebase/app@0.14.3) + '@firebase/functions-types': 0.6.3 + '@firebase/util': 1.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/functions-types@0.6.3': {} + + '@firebase/functions@0.13.1(@firebase/app@0.14.3)': + dependencies: + '@firebase/app': 0.14.3 + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.7.0 + '@firebase/messaging-interop-types': 0.2.3 + '@firebase/util': 1.13.0 + tslib: 2.8.1 + + '@firebase/installations-compat@0.2.19(@firebase/app-compat@0.5.3)(@firebase/app-types@0.9.3)(@firebase/app@0.14.3)': + dependencies: + '@firebase/app-compat': 0.5.3 + '@firebase/component': 0.7.0 + '@firebase/installations': 0.6.19(@firebase/app@0.14.3) + '@firebase/installations-types': 0.5.3(@firebase/app-types@0.9.3) + '@firebase/util': 1.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/installations-types@0.5.3(@firebase/app-types@0.9.3)': + dependencies: + '@firebase/app-types': 0.9.3 + + '@firebase/installations@0.6.19(@firebase/app@0.14.3)': + dependencies: + '@firebase/app': 0.14.3 + '@firebase/component': 0.7.0 + '@firebase/util': 1.13.0 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/logger@0.5.0': + dependencies: + tslib: 2.8.1 + + '@firebase/messaging-compat@0.2.23(@firebase/app-compat@0.5.3)(@firebase/app@0.14.3)': + dependencies: + '@firebase/app-compat': 0.5.3 + '@firebase/component': 0.7.0 + '@firebase/messaging': 0.12.23(@firebase/app@0.14.3) + '@firebase/util': 1.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/messaging-interop-types@0.2.3': {} + + '@firebase/messaging@0.12.23(@firebase/app@0.14.3)': + dependencies: + '@firebase/app': 0.14.3 + '@firebase/component': 0.7.0 + '@firebase/installations': 0.6.19(@firebase/app@0.14.3) + '@firebase/messaging-interop-types': 0.2.3 + '@firebase/util': 1.13.0 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/performance-compat@0.2.22(@firebase/app-compat@0.5.3)(@firebase/app@0.14.3)': + dependencies: + '@firebase/app-compat': 0.5.3 + '@firebase/component': 0.7.0 + '@firebase/logger': 0.5.0 + '@firebase/performance': 0.7.9(@firebase/app@0.14.3) + '@firebase/performance-types': 0.2.3 + '@firebase/util': 1.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/performance-types@0.2.3': {} + + '@firebase/performance@0.7.9(@firebase/app@0.14.3)': + dependencies: + '@firebase/app': 0.14.3 + '@firebase/component': 0.7.0 + '@firebase/installations': 0.6.19(@firebase/app@0.14.3) + '@firebase/logger': 0.5.0 + '@firebase/util': 1.13.0 + tslib: 2.8.1 + web-vitals: 4.2.4 + + '@firebase/remote-config-compat@0.2.20(@firebase/app-compat@0.5.3)(@firebase/app@0.14.3)': + dependencies: + '@firebase/app-compat': 0.5.3 + '@firebase/component': 0.7.0 + '@firebase/logger': 0.5.0 + '@firebase/remote-config': 0.7.0(@firebase/app@0.14.3) + '@firebase/remote-config-types': 0.5.0 + '@firebase/util': 1.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/remote-config-types@0.5.0': {} + + '@firebase/remote-config@0.7.0(@firebase/app@0.14.3)': + dependencies: + '@firebase/app': 0.14.3 + '@firebase/component': 0.7.0 + '@firebase/installations': 0.6.19(@firebase/app@0.14.3) + '@firebase/logger': 0.5.0 + '@firebase/util': 1.13.0 + tslib: 2.8.1 + + '@firebase/storage-compat@0.4.0(@firebase/app-compat@0.5.3)(@firebase/app-types@0.9.3)(@firebase/app@0.14.3)': + dependencies: + '@firebase/app-compat': 0.5.3 + '@firebase/component': 0.7.0 + '@firebase/storage': 0.14.0(@firebase/app@0.14.3) + '@firebase/storage-types': 0.8.3(@firebase/app-types@0.9.3)(@firebase/util@1.13.0) + '@firebase/util': 1.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/storage-types@0.8.3(@firebase/app-types@0.9.3)(@firebase/util@1.13.0)': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.13.0 + + '@firebase/storage@0.14.0(@firebase/app@0.14.3)': + dependencies: + '@firebase/app': 0.14.3 + '@firebase/component': 0.7.0 + '@firebase/util': 1.13.0 + tslib: 2.8.1 + + '@firebase/util@1.13.0': + dependencies: + tslib: 2.8.1 + + '@firebase/webchannel-wrapper@1.0.5': {} + + '@grpc/grpc-js@1.9.15': + dependencies: + '@grpc/proto-loader': 0.7.15 + '@types/node': 24.6.2 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -3479,6 +4148,29 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@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': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 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': {} + '@rolldown/pluginutils@1.0.0-beta.35': {} '@rollup/plugin-babel@5.3.1(@babel/core@7.28.4)(rollup@2.79.2)': @@ -3715,12 +4407,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.13 '@tailwindcss/oxide-win32-x64-msvc': 4.1.13 - '@tailwindcss/vite@4.1.13(vite@7.1.6(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0))': + '@tailwindcss/vite@4.1.13(vite@7.1.6(@types/node@24.6.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0))': dependencies: '@tailwindcss/node': 4.1.13 '@tailwindcss/oxide': 4.1.13 tailwindcss: 4.1.13 - vite: 7.1.6(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0) + vite: 7.1.6(@types/node@24.6.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0) '@types/estree@0.0.39': {} @@ -3728,6 +4420,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/node@24.6.2': + dependencies: + undici-types: 7.13.0 + '@types/react-dom@19.1.9(@types/react@19.1.13)': dependencies: '@types/react': 19.1.13 @@ -3837,11 +4533,11 @@ snapshots: optionalDependencies: react: 19.1.1 - '@vitejs/plugin-react-swc@4.1.0(vite@7.1.6(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0))': + '@vitejs/plugin-react-swc@4.1.0(vite@7.1.6(@types/node@24.6.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.35 '@swc/core': 1.13.5 - vite: 7.1.6(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0) + vite: 7.1.6(@types/node@24.6.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0) transitivePeerDependencies: - '@swc/helpers' @@ -3865,6 +4561,8 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-regex@5.0.1: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -3975,6 +4673,12 @@ snapshots: chownr@3.0.0: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -4057,6 +4761,8 @@ snapshots: electron-to-chromium@1.5.221: {} + emoji-regex@8.0.0: {} + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 @@ -4274,6 +4980,10 @@ snapshots: dependencies: reusify: 1.1.0 + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.4 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -4295,6 +5005,39 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + firebase@12.3.0: + dependencies: + '@firebase/ai': 2.3.0(@firebase/app-types@0.9.3)(@firebase/app@0.14.3) + '@firebase/analytics': 0.10.18(@firebase/app@0.14.3) + '@firebase/analytics-compat': 0.2.24(@firebase/app-compat@0.5.3)(@firebase/app@0.14.3) + '@firebase/app': 0.14.3 + '@firebase/app-check': 0.11.0(@firebase/app@0.14.3) + '@firebase/app-check-compat': 0.4.0(@firebase/app-compat@0.5.3)(@firebase/app@0.14.3) + '@firebase/app-compat': 0.5.3 + '@firebase/app-types': 0.9.3 + '@firebase/auth': 1.11.0(@firebase/app@0.14.3) + '@firebase/auth-compat': 0.6.0(@firebase/app-compat@0.5.3)(@firebase/app-types@0.9.3)(@firebase/app@0.14.3) + '@firebase/data-connect': 0.3.11(@firebase/app@0.14.3) + '@firebase/database': 1.1.0 + '@firebase/database-compat': 2.1.0 + '@firebase/firestore': 4.9.2(@firebase/app@0.14.3) + '@firebase/firestore-compat': 0.4.2(@firebase/app-compat@0.5.3)(@firebase/app-types@0.9.3)(@firebase/app@0.14.3) + '@firebase/functions': 0.13.1(@firebase/app@0.14.3) + '@firebase/functions-compat': 0.4.1(@firebase/app-compat@0.5.3)(@firebase/app@0.14.3) + '@firebase/installations': 0.6.19(@firebase/app@0.14.3) + '@firebase/installations-compat': 0.2.19(@firebase/app-compat@0.5.3)(@firebase/app-types@0.9.3)(@firebase/app@0.14.3) + '@firebase/messaging': 0.12.23(@firebase/app@0.14.3) + '@firebase/messaging-compat': 0.2.23(@firebase/app-compat@0.5.3)(@firebase/app@0.14.3) + '@firebase/performance': 0.7.9(@firebase/app@0.14.3) + '@firebase/performance-compat': 0.2.22(@firebase/app-compat@0.5.3)(@firebase/app@0.14.3) + '@firebase/remote-config': 0.7.0(@firebase/app@0.14.3) + '@firebase/remote-config-compat': 0.2.20(@firebase/app-compat@0.5.3)(@firebase/app@0.14.3) + '@firebase/storage': 0.14.0(@firebase/app@0.14.3) + '@firebase/storage-compat': 0.4.0(@firebase/app-compat@0.5.3)(@firebase/app-types@0.9.3)(@firebase/app@0.14.3) + '@firebase/util': 1.13.0 + transitivePeerDependencies: + - '@react-native-async-storage/async-storage' + flat-cache@4.0.1: dependencies: flatted: 3.3.3 @@ -4333,6 +5076,8 @@ snapshots: gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4413,6 +5158,8 @@ snapshots: dependencies: function-bind: 1.1.2 + http-parser-js@0.5.10: {} + idb@7.1.1: {} ignore@5.3.2: {} @@ -4485,6 +5232,8 @@ snapshots: dependencies: call-bound: 1.0.4 + is-fullwidth-code-point@3.0.0: {} + is-generator-function@1.1.0: dependencies: call-bound: 1.0.4 @@ -4656,6 +5405,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.camelcase@4.3.0: {} + lodash.debounce@4.0.8: {} lodash.merge@4.6.2: {} @@ -4664,6 +5415,8 @@ snapshots: lodash@4.17.21: {} + long@5.3.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -4785,6 +5538,21 @@ snapshots: pretty-bytes@6.1.1: {} + protobufjs@7.5.4: + 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': 24.6.2 + long: 5.3.2 + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -4860,6 +5628,8 @@ snapshots: dependencies: jsesc: 3.0.2 + require-directory@2.1.1: {} + require-from-string@2.0.2: {} resolve-from@4.0.0: {} @@ -5018,6 +5788,12 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + string.prototype.matchall@4.0.12: dependencies: call-bind: 1.0.8 @@ -5063,6 +5839,10 @@ snapshots: is-obj: 1.0.1 is-regexp: 1.0.0 + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + strip-comments@2.0.1: {} strip-json-comments@3.1.1: {} @@ -5119,6 +5899,8 @@ snapshots: dependencies: typescript: 5.9.2 + tslib@2.8.1: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -5178,6 +5960,8 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + undici-types@7.13.0: {} + unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-match-property-ecmascript@2.0.0: @@ -5207,18 +5991,18 @@ snapshots: dependencies: punycode: 2.3.1 - vite-plugin-pwa@1.0.3(vite@7.1.6(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0))(workbox-build@7.3.0)(workbox-window@7.3.0): + vite-plugin-pwa@1.0.3(vite@7.1.6(@types/node@24.6.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0))(workbox-build@7.3.0)(workbox-window@7.3.0): dependencies: debug: 4.4.3 pretty-bytes: 6.1.1 tinyglobby: 0.2.15 - vite: 7.1.6(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0) + vite: 7.1.6(@types/node@24.6.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0) workbox-build: 7.3.0 workbox-window: 7.3.0 transitivePeerDependencies: - supports-color - vite@7.1.6(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0): + vite@7.1.6(@types/node@24.6.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0): dependencies: esbuild: 0.25.10 fdir: 6.5.0(picomatch@4.0.3) @@ -5227,13 +6011,24 @@ snapshots: rollup: 4.50.2 tinyglobby: 0.2.15 optionalDependencies: + '@types/node': 24.6.2 fsevents: 2.3.3 jiti: 2.5.1 lightningcss: 1.30.1 terser: 5.44.0 + web-vitals@4.2.4: {} + webidl-conversions@4.0.2: {} + websocket-driver@0.7.4: + dependencies: + http-parser-js: 0.5.10 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + whatwg-url@7.1.0: dependencies: lodash.sortby: 4.7.0 @@ -5400,10 +6195,30 @@ snapshots: '@types/trusted-types': 2.0.7 workbox-core: 7.3.0 + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrappy@1.0.2: {} + y18n@5.0.8: {} + yallist@3.1.1: {} yallist@5.0.0: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yocto-queue@0.1.0: {} diff --git a/site/public/firebase-messaging-sw.js b/site/public/firebase-messaging-sw.js new file mode 100644 index 0000000..056ce2b --- /dev/null +++ b/site/public/firebase-messaging-sw.js @@ -0,0 +1,143 @@ +importScripts('https://www.gstatic.com/firebasejs/9.23.0/firebase-app-compat.js'); +importScripts('https://www.gstatic.com/firebasejs/9.23.0/firebase-messaging-compat.js'); + +firebase.initializeApp({ + apiKey: "AIzaSyAkxH71PlZJxhD7vuN_Q8kn3TtNnB09_cU", + authDomain: "updates-9eab8.firebaseapp.com", + projectId: "updates-9eab8", + storageBucket: "updates-9eab8.firebasestorage.app", + messagingSenderId: "347275855103", + appId: "1:347275855103:web:fb59a7504792c2736538ca" +}); + +const messaging = firebase.messaging(); + +// Handle background messages +messaging.onBackgroundMessage(function(payload) { + console.log('[firebase-messaging-sw.js] Received background message', payload); + + // Extract notification data + const notificationTitle = payload.notification?.title || 'New Update'; + const notificationBody = payload.notification?.body || 'You have a new notification'; + + // Build notification options with enhanced features + const notificationOptions = { + body: notificationBody, + icon: payload.notification?.icon || '/android/android-launchericon-192-192.png', + badge: '/android/android-launchericon-72-72.png', + vibrate: [200, 100, 200], + tag: payload.data?.tag || 'default-tag', + requireInteraction: payload.data?.requireInteraction === 'true', + renotify: true, + silent: false, + timestamp: Date.now(), + data: { + url: payload.data?.url || '/', + gameId: payload.data?.gameId, + ...payload.data + } + }; + + // Add image if provided + if (payload.notification?.image) { + notificationOptions.image = payload.notification.image; + } + + // Add actions if provided + if (payload.data?.actions) { + try { + notificationOptions.actions = JSON.parse(payload.data.actions); + } catch (e) { + console.error('Failed to parse notification actions:', e); + } + } + + // Show the notification + return self.registration.showNotification(notificationTitle, notificationOptions); +}); + +// Handle notification clicks +self.addEventListener('notificationclick', function(event) { + console.log('[firebase-messaging-sw.js] Notification click received.', event); + + event.notification.close(); + + // Handle action clicks + if (event.action) { + console.log('Action clicked:', event.action); + // You can handle different actions here + if (event.action === 'view') { + event.waitUntil( + clients.openWindow(event.notification.data?.url || '/') + ); + } else if (event.action === 'dismiss') { + // Just close the notification + return; + } + } else { + // Default click behavior - open the URL + const clickUrl = event.notification.data?.url || '/'; + + event.waitUntil( + clients.matchAll({ + type: 'window', + includeUncontrolled: true + }).then(function(clientList) { + // Check if there's already a window/tab open with the target URL + for (const client of clientList) { + if (client.url === clickUrl && 'focus' in client) { + return client.focus(); + } + } + // If no existing window/tab, open a new one + if (clients.openWindow) { + return clients.openWindow(clickUrl); + } + }) + ); + } +}); + +// Handle notification close +self.addEventListener('notificationclose', function(event) { + console.log('[firebase-messaging-sw.js] Notification was closed', event); + // You can track notification dismissals here if needed +}); + +// Handle service worker installation +self.addEventListener('install', function(event) { + console.log('[firebase-messaging-sw.js] Service Worker installing.'); + self.skipWaiting(); +}); + +// Handle service worker activation +self.addEventListener('activate', function(event) { + console.log('[firebase-messaging-sw.js] Service Worker activated.'); + event.waitUntil(clients.claim()); +}); + +// Handle push events (for debugging) +self.addEventListener('push', function(event) { + console.log('[firebase-messaging-sw.js] Push event received', event); + + if (event.data) { + try { + const data = event.data.json(); + console.log('[firebase-messaging-sw.js] Push data:', data); + } catch (e) { + console.log('[firebase-messaging-sw.js] Push data text:', event.data.text()); + } + } +}); + +// Error handling +self.addEventListener('error', function(event) { + console.error('[firebase-messaging-sw.js] Service Worker error:', event); +}); + +// Fetch event handler for offline support (optional) +self.addEventListener('fetch', function(event) { + // You can add offline caching strategies here if needed + // For now, just pass through the request + return; +});
\ No newline at end of file diff --git a/site/src/components/NotificationButton.tsx b/site/src/components/NotificationButton.tsx new file mode 100644 index 0000000..8f4fb61 --- /dev/null +++ b/site/src/components/NotificationButton.tsx @@ -0,0 +1,196 @@ +import { useState, useEffect } from "react"; +import { messaging, initializeForegroundNotifications } from "../firebase.ts"; +import { getToken, deleteToken } from "firebase/messaging"; + +const VAPID_KEY = + "BK7tpLF5Loy8Ew8bKxhTi-vOEJdxJSnu-jPyagWecLdD_SrEAt_OQS7nu0Xu3hR7AQpn0cOmgcdeeQd5zq5-Gyo"; + +interface NotificationButtonProps { + className?: string; + isMoe?: boolean; +} + +export default function NotificationButton({ className = "", isMoe = false }: NotificationButtonProps) { + const [permission, setPermission] = useState<NotificationPermission>("default"); + const [isRegistered, setIsRegistered] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState<string | null>(null); + + useEffect(() => { + // Check initial permission status + setPermission(Notification.permission); + + // Check if service worker is registered + const checkRegistration = async () => { + if ('serviceWorker' in navigator) { + const registration = await navigator.serviceWorker.getRegistration('/firebase-messaging-sw.js'); + setIsRegistered(!!registration); + + // Initialize foreground notifications if already registered + if (registration && Notification.permission === "granted") { + initializeForegroundNotifications(); + } + } + }; + + checkRegistration(); + }, []); + + const handleEnableNotifications = async () => { + setLoading(true); + setError(null); + + try { + const permissionResult = await Notification.requestPermission(); + setPermission(permissionResult); + + if (permissionResult === "granted") { + // Register service worker + const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js'); + console.log("Service Worker registered:", registration); + const token = await getToken(messaging, { vapidKey: VAPID_KEY }); + console.log("FCM Token:", token); + // Store token locally (you might want to send this to your server) + localStorage.setItem('fcm_token', token); + + // Initialize foreground notification handler + initializeForegroundNotifications(); + + setIsRegistered(true); + } else { + setError("Notification permission was denied"); + } + } catch (err) { + console.error("Error enabling notifications:", err); + setError("Failed to enable notifications. Please try again."); + } finally { + setLoading(false); + } + }; + + const handleDisableNotifications = async () => { + setLoading(true); + setError(null); + + try { + await deleteToken(messaging); + console.log("FCM token deleted"); + localStorage.removeItem('fcm_token'); + if ('serviceWorker' in navigator) { + const registration = await navigator.serviceWorker.getRegistration('/firebase-messaging-sw.js'); + if (registration) { + await registration.unregister(); + console.log("Service Worker unregistered"); + } + } + + setIsRegistered(false); + } catch (err) { + console.error("Error disabling notifications:", err); + setError("Failed to disable notifications. Please try again."); + } finally { + setLoading(false); + } + }; + + // Determine button state and action + const getButtonContent = () => { + if (loading) { + return ( + <> + <svg className="animate-spin h-5 w-5 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> + <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> + <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> + </svg> + {isRegistered ? "Disabling..." : "Enabling..."} + </> + ); + } + + if (permission === "denied") { + return ( + <> + <svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" /> + </svg> + Notifications Blocked + </> + ); + } + + if (isRegistered && permission === "granted") { + return ( + <> + <svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24"> + <path d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" /> + </svg> + Disable Notifications + </> + ); + } + + return ( + <> + <svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" /> + </svg> + Enable Notifications + </> + ); + }; + + const handleClick = () => { + if (permission === "denied") { + // Can't re-request permission if denied + alert("Notifications are blocked. Please enable them in your browser settings."); + return; + } + + if (isRegistered && permission === "granted") { + handleDisableNotifications(); + } else { + handleEnableNotifications(); + } + }; + + // Determine button styles + const getButtonStyles = () => { + if (loading || permission === "denied") { + return isMoe + ? `bg-pink-300 cursor-not-allowed opacity-60` + : `bg-gray-600 cursor-not-allowed opacity-60`; + } + + if (isMoe) { + return isRegistered + ? `bg-pink-600 text-white hover:bg-pink-700` + : `bg-pink-500 text-white hover:bg-pink-600`; + } else { + return isRegistered + ? `bg-purple-700 text-white hover:bg-purple-800` + : `bg-purple-600 text-white hover:bg-purple-700`; + } + }; + + return ( + <div className="flex flex-col items-center gap-2"> + <button + onClick={handleClick} + disabled={loading || permission === "denied"} + className={`flex items-center justify-center px-4 py-2 rounded-lg font-semibold transition-colors ${getButtonStyles()} ${className}`} + > + {getButtonContent()} + </button> + {error && ( + <p className={`text-sm ${isMoe ? "text-pink-600" : "text-red-500"}`}> + {error} + </p> + )} + {permission === "denied" && ( + <p className={`text-xs ${isMoe ? "text-pink-600" : "text-gray-400"}`}> + To enable notifications, update your browser settings + </p> + )} + </div> + ); +} diff --git a/site/src/firebase.ts b/site/src/firebase.ts new file mode 100644 index 0000000..e908e58 --- /dev/null +++ b/site/src/firebase.ts @@ -0,0 +1,81 @@ +import { initializeApp } from "firebase/app"; +import { getMessaging, Messaging, onMessage } from "firebase/messaging"; + +const firebaseConfig = { + apiKey: "AIzaSyAkxH71PlZJxhD7vuN_Q8kn3TtNnB09_cU", + authDomain: "updates-9eab8.firebaseapp.com", + projectId: "updates-9eab8", + storageBucket: "updates-9eab8.firebasestorage.app", + messagingSenderId: "347275855103", + appId: "1:347275855103:web:fb59a7504792c2736538ca" +}; + +const app = initializeApp(firebaseConfig); + +export const messaging: Messaging = getMessaging(app); + +// Handle foreground messages +export const initializeForegroundNotifications = () => { + onMessage(messaging, (payload) => { + console.log('[firebase.ts] Message received in foreground:', payload); + + // Check if browser supports notifications + if (!("Notification" in window)) { + console.log("This browser does not support desktop notifications"); + return; + } + + // Check notification permission + if (Notification.permission === "granted") { + // Create notification + const notificationTitle = payload.notification?.title || 'New Update'; + const notificationOptions: NotificationOptions = { + body: payload.notification?.body || 'You have a new notification', + icon: payload.notification?.icon || '/android/android-launchericon-192-192.png', + badge: '/android/android-launchericon-72-72.png', + tag: payload.data?.tag || 'default-tag', + requireInteraction: payload.data?.requireInteraction === 'true', + silent: false, + data: { + url: payload.data?.url || '/', + gameId: payload.data?.gameId, + ...payload.data + } + }; + + // Add image if provided + if (payload.notification?.image) { + notificationOptions.badge = payload.notification.image; + } + + // Create and show the notification + const notification = new Notification(notificationTitle, notificationOptions); + + // Handle notification click + notification.onclick = (event) => { + event.preventDefault(); + notification.close(); + + // Navigate to the URL if provided + const url = payload.data?.url || '/'; + window.open(url, '_blank'); + }; + + // Handle notification error + notification.onerror = (event) => { + console.error('[firebase.ts] Notification error:', event); + }; + + // Auto-close notification after 10 seconds if not require interaction + if (payload.data?.requireInteraction !== 'true') { + setTimeout(() => { + notification.close(); + }, 10000); + } + } else { + console.log('[firebase.ts] Notification permission not granted'); + } + }); + + console.log('[firebase.ts] Foreground notification handler initialized'); +}; diff --git a/site/src/pages/Homepage.tsx b/site/src/pages/Homepage.tsx index 2f35280..db658ca 100644 --- a/site/src/pages/Homepage.tsx +++ b/site/src/pages/Homepage.tsx @@ -4,6 +4,7 @@ import { useParams, useSearchParams } from "react-router-dom"; import { getGameTitle } from "../utils.ts"; import TitleBar from "../components/TitleBar"; import { GameNotes } from "../components/GameNotes"; +import NotificationButton from "../components/NotificationButton"; interface ArcadeNewsAPIData { fetch_time: number; @@ -14,8 +15,9 @@ export default function Home() { const { gameId } = useParams<{ gameId?: string }>(); const [searchParams] = useSearchParams(); const isMoe = searchParams.has("moe"); - const rssFeedUrl = import.meta.env.VITE_NEWS_BASE_URL + "/" +gameId + "_news.xml"; - const newsAPIBase = import.meta.env.VITE_NEWS_BASE_URL + const rssFeedUrl = + import.meta.env.VITE_NEWS_BASE_URL + "/" + gameId + "_news.xml"; + const newsAPIBase = import.meta.env.VITE_NEWS_BASE_URL; const [newsFeedData, setNewsFeedData] = useState<ArcadeNewsAPIData | null>( null, @@ -29,9 +31,7 @@ export default function Home() { setError(false); const newsDataFileName = gameId ? `${gameId}_news.json` : "news.json"; try { - const response = await fetch( - newsAPIBase+"/" + `${newsDataFileName}`, - ); + const response = await fetch(newsAPIBase + "/" + `${newsDataFileName}`); if (!response.ok) { throw new Error(`Failed to fetch news: ${response.statusText}`); } @@ -70,21 +70,31 @@ export default function Home() { className={`${isMoe ? "bg-pink-100" : "bg-gray-950"} min-h-screen flex items-center justify-center`} > <div className="text-center px-4"> - <div className={`${isMoe ? "text-pink-500" : "text-purple-500"} text-8xl font-bold mb-4`}> + <div + className={`${isMoe ? "text-pink-500" : "text-purple-500"} text-8xl font-bold mb-4`} + > 404 </div> - <h1 className={`${isMoe ? "text-pink-900" : "text-white"} text-3xl font-bold mb-4`}> + <h1 + className={`${isMoe ? "text-pink-900" : "text-white"} text-3xl font-bold mb-4`} + > News Not Found </h1> - <p className={`${isMoe ? "text-pink-700" : "text-gray-400"} text-lg mb-8`}> + <p + className={`${isMoe ? "text-pink-700" : "text-gray-400"} text-lg mb-8`} + > {gameId ? `Unable to fetch news for ${getGameTitle(gameId)}` - : "Unable to fetch news feed" - } + : "Unable to fetch news feed"} </p> - <div className={`${isMoe ? "bg-pink-200" : "bg-gray-800"} rounded-lg p-6 max-w-md mx-auto`}> - <p className={`${isMoe ? "text-pink-800" : "text-gray-300"} mb-4`}> - The news feed you're looking for might be temporarily unavailable or doesn't exist. + <div + className={`${isMoe ? "bg-pink-200" : "bg-gray-800"} rounded-lg p-6 max-w-md mx-auto`} + > + <p + className={`${isMoe ? "text-pink-800" : "text-gray-300"} mb-4`} + > + The news feed you're looking for might be temporarily + unavailable or doesn't exist. </p> <a href="/" @@ -127,43 +137,64 @@ export default function Home() { </div> </div> ) : ( - <div - className={`${isMoe ? "bg-pink-200 text-pink-900" : "bg-gray-800 text-white"} rounded-lg p-6 text-center shadow-lg`} - > - <h1 className="text-2xl font-bold">Welcome to 573-UPDATES</h1> - <h2 - className={`text-2xl font-extrabold mb-4 tracking-widest text-center uppercase glow-neon ${ - isMoe ? "text-pink-500" : "text-[#00FF00]" - }`} + <> + <div + className={`${isMoe ? "bg-pink-200 text-pink-900" : "bg-gray-800 text-white"} rounded-lg p-6 text-center shadow-lg`} > - SECOND STYLE - </h2> - <div className="floating"> - <img - src="/liris.webp" - className="w-48 mx-auto mb-2 object-contain rounded-2xl" - /> - </div> - <p> - News and Information for various arcade games is aggregated - here! - </p> - <p className="mt-2"> - RSS feeds are available on each game's individual page - </p> - <p className="mt-2"> - Please see the{" "} - <a - href="https://github.com/pinapelz/573-updates" - className="text-blue-500 hover:underline" + <h1 className="text-2xl font-bold">Welcome to 573-UPDATES</h1> + <h2 + className={`text-2xl font-extrabold mb-4 tracking-widest text-center uppercase glow-neon ${ + isMoe ? "text-pink-500" : "text-[#00FF00]" + }`} > - GitHub - </a>{" "} - for API information - </p> - </div> + SECOND STYLE + </h2> + <div className="floating"> + <img + src="/liris.webp" + className="w-48 mx-auto mb-2 object-contain rounded-2xl" + /> + </div> + <p> + News and Information for various arcade games is aggregated + here! + </p> + <p className="mt-2"> + RSS feeds are available on each game's individual page + </p> + <p className="mt-2"> + Please see the{" "} + <a + href="https://github.com/pinapelz/573-updates" + className="text-blue-500 hover:underline" + > + GitHub + </a>{" "} + for API information + </p> + <div className="mt-6"> + <details className="rounded-lg"> + <summary + className={`cursor-pointer text-lg font-semibold ${ + isMoe + ? "text-pink-700 hover:text-pink-500" + : "text-gray-300 hover:text-white" + }`} + > + Receive Notifications + </summary> + <div className="mt-4"> + <NotificationButton isMoe={isMoe} /> + <p className="mt-2"> + Enables notifications for the main feed + </p> + </div> + </details> + </div> + </div> + </> )} - <NewsFeed newsItems={newsFeedData.news_posts}/> + <NewsFeed newsItems={newsFeedData.news_posts} /> </div> <footer className={`mt-8 text-center text-sm ${isMoe ? "text-pink-800" : "text-gray-400"}`} |
