aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2025-10-03 03:51:40 -0700
committerPinapelz <yukais@pinapelz.com>2025-10-03 03:51:40 -0700
commitab8d1306504bfefdea293172d7e645f83114d50f (patch)
treef6108ac6ea110819ed23a80e87068c3f4538c71b
parent98ead632b7715b8f1768c962a37b9efa0a684484 (diff)
add topic based stubs for notifications
-rw-r--r--site/src/components/NotificationButton.tsx152
-rw-r--r--site/src/pages/Homepage.tsx103
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>
</>
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage