aboutsummaryrefslogtreecommitdiffstats
path: root/site/src
diff options
context:
space:
mode:
Diffstat (limited to 'site/src')
-rw-r--r--site/src/components/NotificationButton.tsx196
-rw-r--r--site/src/firebase.ts81
-rw-r--r--site/src/pages/Homepage.tsx125
3 files changed, 355 insertions, 47 deletions
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"}`}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage