From 3d1d33c2aac15e07c3b840a1fb9428e3feda8330 Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Fri, 3 Oct 2025 02:25:55 -0700 Subject: initial firebase fcm implementation --- site/src/components/NotificationButton.tsx | 196 +++++++++++++++++++++++++++++ site/src/firebase.ts | 81 ++++++++++++ site/src/pages/Homepage.tsx | 125 +++++++++++------- 3 files changed, 355 insertions(+), 47 deletions(-) create mode 100644 site/src/components/NotificationButton.tsx create mode 100644 site/src/firebase.ts (limited to 'site/src') 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("default"); + const [isRegistered, setIsRegistered] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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 ( + <> + + + + + {isRegistered ? "Disabling..." : "Enabling..."} + + ); + } + + if (permission === "denied") { + return ( + <> + + + + Notifications Blocked + + ); + } + + if (isRegistered && permission === "granted") { + return ( + <> + + + + Disable Notifications + + ); + } + + return ( + <> + + + + 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 ( +
+ + {error && ( +

+ {error} +

+ )} + {permission === "denied" && ( +

+ To enable notifications, update your browser settings +

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

+

News Not Found

-

+

{gameId ? `Unable to fetch news for ${getGameTitle(gameId)}` - : "Unable to fetch news feed" - } + : "Unable to fetch news feed"}

-
-

- The news feed you're looking for might be temporarily unavailable or doesn't exist. +

+

+ The news feed you're looking for might be temporarily + unavailable or doesn't exist.

) : ( - + SECOND STYLE + +
+ +
+

+ News and Information for various arcade games is aggregated + here! +

+

+ RSS feeds are available on each game's individual page +

+

+ Please see the{" "} + + GitHub + {" "} + for API information +

+
+
+ + Receive Notifications + +
+ +

+ Enables notifications for the main feed +

+
+
+
+
+ )} - +