diff options
| author | Pinapelz <yukais@pinapelz.com> | 2025-10-03 03:51:40 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2025-10-03 03:51:40 -0700 |
| commit | ab8d1306504bfefdea293172d7e645f83114d50f (patch) | |
| tree | f6108ac6ea110819ed23a80e87068c3f4538c71b | |
| parent | 98ead632b7715b8f1768c962a37b9efa0a684484 (diff) | |
add topic based stubs for notifications
| -rw-r--r-- | site/src/components/NotificationButton.tsx | 152 | ||||
| -rw-r--r-- | site/src/pages/Homepage.tsx | 103 |
2 files changed, 226 insertions, 29 deletions
diff --git a/site/src/components/NotificationButton.tsx b/site/src/components/NotificationButton.tsx index 66109a3..3e0342b 100644 --- a/site/src/components/NotificationButton.tsx +++ b/site/src/components/NotificationButton.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from "react"; import { messaging, initializeForegroundNotifications } from "../firebase.ts"; import { getToken, deleteToken } from "firebase/messaging"; +import { getGameTitle } from "../utils.ts"; const VAPID_KEY = "BK7tpLF5Loy8Ew8bKxhTi-vOEJdxJSnu-jPyagWecLdD_SrEAt_OQS7nu0Xu3hR7AQpn0cOmgcdeeQd5zq5-Gyo"; @@ -8,11 +9,13 @@ const VAPID_KEY = interface NotificationButtonProps { className?: string; isMoe?: boolean; + gameId?: string; } -export default function NotificationButton({ className = "", isMoe = false }: NotificationButtonProps) { +export default function NotificationButton({ className = "", isMoe = false, gameId }: NotificationButtonProps) { const [permission, setPermission] = useState<NotificationPermission>("default"); const [isRegistered, setIsRegistered] = useState(false); + const [isSubscribed, setIsSubscribed] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); @@ -33,8 +36,91 @@ export default function NotificationButton({ className = "", isMoe = false }: No } }; + // Check if subscribed to topic (for gameId mode) + const checkTopicSubscription = () => { + if (gameId) { + const subscribedTopics = JSON.parse(localStorage.getItem('subscribed_topics') || '[]'); + setIsSubscribed(subscribedTopics.includes(gameId)); + } + }; + checkRegistration(); - }, []); + checkTopicSubscription(); + }, [gameId]); + + const handleSubscribeToTopic = async () => { + setLoading(true); + setError(null); + + try { + // Ensure notifications are enabled first + const permissionResult = await Notification.requestPermission(); + setPermission(permissionResult); + + if (permissionResult === "granted") { + let registration = await navigator.serviceWorker.getRegistration('/firebase-messaging-sw.js'); + if (!registration) { + registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js'); + console.log("Service Worker registered:", registration); + } + + let token = localStorage.getItem('fcm_token'); + if (!token) { + token = await getToken(messaging, { + vapidKey: VAPID_KEY, + serviceWorkerRegistration: registration, + }); + localStorage.setItem('fcm_token', token); + } + + // TODO: Subscribe to topic via backend API + console.log(`Subscribing to topic: ${gameId} with token: ${token}`); + // Stub for now - will make actual API call to backend to subscribe to topic + + // Update local storage to track subscribed topics + const subscribedTopics = JSON.parse(localStorage.getItem('subscribed_topics') || '[]'); + if (!subscribedTopics.includes(gameId)) { + subscribedTopics.push(gameId); + localStorage.setItem('subscribed_topics', JSON.stringify(subscribedTopics)); + } + + setIsSubscribed(true); + setIsRegistered(true); + } else { + setError("Notification permission was denied"); + } + } catch (err) { + console.error("Error subscribing to topic:", err); + setError("Failed to subscribe to game notifications. Please try again."); + } finally { + setLoading(false); + } + }; + + const handleUnsubscribeFromTopic = async () => { + setLoading(true); + setError(null); + + try { + const token = localStorage.getItem('fcm_token'); + + // TODO: Unsubscribe from topic via backend API + console.log(`Unsubscribing from topic: ${gameId} with token: ${token}`); + // Stub for now - will make actual API call to backend to unsubscribe from topic + + // Update local storage to remove topic + const subscribedTopics = JSON.parse(localStorage.getItem('subscribed_topics') || '[]'); + const updatedTopics = subscribedTopics.filter((topic: string) => topic !== gameId); + localStorage.setItem('subscribed_topics', JSON.stringify(updatedTopics)); + + setIsSubscribed(false); + } catch (err) { + console.error("Error unsubscribing from topic:", err); + setError("Failed to unsubscribe from game notifications. Please try again."); + } finally { + setLoading(false); + } + }; const handleEnableNotifications = async () => { setLoading(true); @@ -94,6 +180,7 @@ export default function NotificationButton({ className = "", isMoe = false }: No } setIsRegistered(false); + setIsSubscribed(false); } catch (err) { console.error("Error disabling notifications:", err); setError("Failed to disable notifications. Please try again."); @@ -111,7 +198,10 @@ export default function NotificationButton({ className = "", isMoe = false }: No <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..."} + {gameId + ? (isSubscribed ? "Unsubscribing..." : "Subscribing...") + : (isRegistered ? "Disabling..." : "Enabling...") + } </> ); } @@ -127,6 +217,28 @@ export default function NotificationButton({ className = "", isMoe = false }: No ); } + // For topic subscription mode + if (gameId) { + if (isSubscribed && permission === "granted") { + return ( + <> + <svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24"> + <path d="M5 13l4 4L19 7" /> + </svg> + Unsubscribe from {getGameTitle(gameId)} + </> + ); + } + 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> + Subscribe to {getGameTitle(gameId)} Updates + </> + ); + } + if (isRegistered && permission === "granted") { return ( <> @@ -154,27 +266,41 @@ export default function NotificationButton({ className = "", isMoe = false }: No return; } - if (isRegistered && permission === "granted") { - handleDisableNotifications(); + if (gameId) { + if (permission !== "granted") { + alert("Please enable general notifications first before subscribing to game updates."); + return; + } + + if (isSubscribed) { + handleUnsubscribeFromTopic(); + } else { + handleSubscribeToTopic(); + } } else { - handleEnableNotifications(); + if (isRegistered && permission === "granted") { + handleDisableNotifications(); + } else { + handleEnableNotifications(); + } } }; - // Determine button styles const getButtonStyles = () => { - if (loading || permission === "denied") { + if (loading || permission === "denied" || (gameId && permission !== "granted")) { return isMoe ? `bg-pink-300 cursor-not-allowed opacity-60` : `bg-gray-600 cursor-not-allowed opacity-60`; } + const isActive = gameId ? isSubscribed : isRegistered; + if (isMoe) { - return isRegistered + return isActive ? `bg-pink-600 text-white hover:bg-pink-700` : `bg-pink-500 text-white hover:bg-pink-600`; } else { - return isRegistered + return isActive ? `bg-purple-700 text-white hover:bg-purple-800` : `bg-purple-600 text-white hover:bg-purple-700`; } @@ -184,7 +310,6 @@ export default function NotificationButton({ className = "", isMoe = false }: No <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()} @@ -199,6 +324,11 @@ export default function NotificationButton({ className = "", isMoe = false }: No To enable notifications, update your browser settings </p> )} + {gameId && permission !== "granted" && permission !== "denied" && ( + <p className={`text-xs ${isMoe ? "text-pink-600" : "text-gray-400"}`}> + Enable general notifications first to subscribe to game updates + </p> + )} </div> ); } diff --git a/site/src/pages/Homepage.tsx b/site/src/pages/Homepage.tsx index db658ca..f9ccaa2 100644 --- a/site/src/pages/Homepage.tsx +++ b/site/src/pages/Homepage.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { NewsData, NewsFeed } from "../components/NewsFeed"; -import { useParams, useSearchParams } from "react-router-dom"; +import { Link, useParams, useSearchParams } from "react-router-dom"; import { getGameTitle } from "../utils.ts"; import TitleBar from "../components/TitleBar"; import { GameNotes } from "../components/GameNotes"; @@ -24,6 +24,29 @@ export default function Home() { ); const [loading, setLoading] = useState<boolean>(true); const [error, setError] = useState<boolean>(false); + const [subscribedGames, setSubscribedGames] = useState<string[]>([]); + const [showSubscribedDropdown, setShowSubscribedDropdown] = useState(false); + + useEffect(() => { + // Load subscribed games from localStorage + const loadSubscribedGames = () => { + const topics = JSON.parse(localStorage.getItem('subscribed_topics') || '[]'); + setSubscribedGames(topics); + }; + + loadSubscribedGames(); + + // Listen for storage changes to update subscribed games list + const handleStorageChange = () => { + loadSubscribedGames(); + }; + + window.addEventListener('storage', handleStorageChange); + + return () => { + window.removeEventListener('storage', handleStorageChange); + }; + }, []); useEffect(() => { const fetchNews = async () => { @@ -47,6 +70,17 @@ export default function Home() { fetchNews(); }, [gameId, newsAPIBase]); // Re-fetch when gameId changes + // Update subscribed games when localStorage changes locally + useEffect(() => { + const checkSubscriptions = () => { + const topics = JSON.parse(localStorage.getItem('subscribed_topics') || '[]'); + setSubscribedGames(topics); + }; + + const interval = setInterval(checkSubscriptions, 500); + return () => clearInterval(interval); + }, []); + if (loading) { return ( <> @@ -130,6 +164,9 @@ export default function Home() { {GameNotes(isMoe)[gameId] && ( <div className="text-left">{GameNotes(isMoe)[gameId]}</div> )} + <div className="mt-4"> + <NotificationButton gameId={gameId} isMoe={isMoe} /> + </div> <div className="mt-2"> <a href={rssFeedUrl} className="text-blue-400 hover:underline"> Subscribe via RSS @@ -173,23 +210,53 @@ export default function Home() { 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 className="mt-4"> + <NotificationButton isMoe={isMoe} /> + + {/* Subscribed Games Display */} + {subscribedGames.length > 0 && ( + <div className="mt-3"> + <button + onClick={() => setShowSubscribedDropdown(!showSubscribedDropdown)} + className={`text-sm ${ + isMoe ? "text-pink-700 hover:text-pink-500" : "text-gray-300 hover:text-white" + } flex items-center gap-1 mx-auto transition-colors`} + > + <span>Subscribed to {subscribedGames.length} game{subscribedGames.length !== 1 ? 's' : ''}</span> + <svg + className={`w-4 h-4 transition-transform ${showSubscribedDropdown ? 'rotate-180' : ''}`} + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + > + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> + </svg> + </button> + + {showSubscribedDropdown && ( + <div className={`mt-2 p-3 rounded-lg ${ + isMoe ? "bg-pink-300 bg-opacity-50" : "bg-gray-700 bg-opacity-50" + }`}> + <div className="grid grid-cols-2 gap-2 text-sm"> + {subscribedGames.map((gameId) => ( + <Link + key={gameId} + to={`/game/${gameId}`} + className={`${ + isMoe + ? "text-pink-700 hover:text-pink-500 hover:bg-pink-200" + : "text-gray-300 hover:text-white hover:bg-gray-600" + } px-2 py-1 rounded transition-all`} + > + {getGameTitle(gameId)} + </Link> + ))} + </div> + </div> + )} + </div> + )} + </div> </div> </div> </> |
