Repository: uoftblueprint/brampton-tennis-queue
Pull request #95: Implement /sendWebNotification API for Push Notifications
Author: login: Divyanshuoft
Created: 2025-03-02T22:58:30Z | Merged: 2025-03-10T17:12:44Z

This PR adds a new API endpoint POST /sendWebNotification, which sends a push notification to the user. It determines whether the user is in the queue or active state and retrieves the correct FCM token (queueTokens[i] or activeTokens[i]) to send the message. The message received in the background will be shown on the foreground.

- The API receives location, uid, and message (title & body)
- It checks if the user is in the queue or active list
- It retrieves the FCM token from either queueTokens[i] or activeTokens[i]
- Sends the notification using Firebase

**Integration with dynamicBuffer**

- This API is now used by dynamicBuffer to notify active players 5 minutes before their session ends, ensuring they are aware of their remaining time

**Integration with advanceQueue**

- The player moving from the queue to active that it’s their turn.
- The next player in line that they are now first in the queue.

API Post request body
```
{
  "location": "Cassie Campbell",
  "uid": "user-id",
  "message": {
    "title": "Game Alert",
    "body": "Your game is about to start!"
  }
}
```

Screenshots:

- user login
<img width="1458" alt="Screenshot 2025-03-02 at 5 27 56 PM" src="https://github.com/user-attachments/assets/fbdaf031-d297-46e8-a4ff-1924d74b043e" />
<img width="1470" alt="Screenshot 2025-03-02 at 5 28 19 PM" src="https://github.com/user-attachments/assets/4fa3d3e6-e8a0-4272-acb9-3cea88a305dc" />


- allow show notifications 
<img width="597" alt="Screenshot 2025-03-02 at 5 28 29 PM" src="https://github.com/user-attachments/assets/0ea0b753-7c12-4114-b55b-76af1b1ef1b3" />


- API request format and successful response format
<img width="1187" alt="Screenshot 2025-03-02 at 5 29 17 PM" src="https://github.com/user-attachments/assets/590b1c2e-57f8-46d6-b0b8-9b79fe998e98" />


- notification push can be seen
<img width="350" alt="Screenshot 2025-03-02 at 5 29 29 PM" src="https://github.com/user-attachments/assets/ba359c1a-071b-4c23-a928-e1699ce51a86" /> <img width="350" alt="Screenshot 2025-03-02 at 5 34 13 PM" src="https://github.com/user-attachments/assets/189293f5-cb68-469a-b939-f1de5ba36ad5" />

These are some of the screenshots that can be seen of how the notification will look like when in use:
<img width="411" alt="Screenshot 2025-03-05 at 9 35 29 PM" src="https://github.com/user-attachments/assets/de2fb139-e784-4e50-9db3-e059b1bad153" />
<img width="421" alt="Screenshot 2025-03-05 at 9 36 01 PM" src="https://github.com/user-attachments/assets/26832dfc-172b-4c8d-9298-11e2a416e39b" />
<img width="627" alt="Screenshot 2025-03-05 at 9 36 05 PM" src="https://github.com/user-attachments/assets/d1962d34-5025-457b-8f6e-0cb45f0dbd22" />


and this is the screenshot for send reminder, as you can see that the player from the queue can send a reminder to the player in the active players that your time's up
<img width="687" alt="Screenshot 2025-03-10 at 12 12 57 AM" src="https://github.com/user-attachments/assets/41d2d90d-844b-4328-ab93-0b46ecd6a6a7" />




Diff:
diff --git a/backend/index.js b/backend/index.js
index 9cf552a..decc8c1 100644
--- a/backend/index.js
+++ b/backend/index.js
@@ -27,6 +27,7 @@ const currentStateRoute = require('./routes/currentStateRoute');
 const joinGameRoute = require('./routes/joinGameRoute');
 const advanceQueueRoute = require('./routes/advanceQueueRoute');
 const trackMetricsRoute = require('./routes/trackMetricsRoute');
+const sendWebNotificationRoute = require('./routes/sendWebNotificationRoute');
 
 // Use imported routes
 app.use('/api', resetCourtsRoute);  // resetCourts endpoint
@@ -40,6 +41,7 @@ app.use('/api', currentStateRoute); // currentState endpoint
 app.use('/api', joinGameRoute); // DUMMY joinGame endpoint - REPLACE WITH ACTUAL CODE IN ROUTE FILE!
 app.use('/api', advanceQueueRoute);  // advanceQueue endpoint
 app.use('/api', trackMetricsRoute);  // trackMetrics endpoint
+app.use('/api', sendWebNotificationRoute);  // sendWebNotification endpoint
 
 // Default route
 app.get('/', (req, res) => {
diff --git a/backend/routes/sendWebNotificationRoute.js b/backend/routes/sendWebNotificationRoute.js
new file mode 100644
index 0000000..342532f
--- /dev/null
+++ b/backend/routes/sendWebNotificationRoute.js
@@ -0,0 +1,31 @@
+const express = require('express');
+const router = express.Router();
+const admin = require('firebase-admin');
+const sendWebNotification = require('../utils/sendNotification');  
+
+router.post('/sendWebNotification', async (req, res) => {
+  try {
+    // Simply extract fields, no need to re-check them here
+    const { location, uid, message } = req.body;
+
+    // Call the utility (it will validate & return success/error objects)
+    const result = await sendWebNotification(location, uid, message);
+
+    // If the utility indicates failure, respond with an error
+    if (!result.success) {
+      return res.status(400).json(result);
+    }
+
+    // Otherwise, respond with success
+    return res.status(200).json(result);
+
+  } catch (error) {
+    console.error("Failed to send notification:", error);
+    return res.status(500).json({
+      message: "Server error",
+      error,
+    });
+  }
+});
+
+module.exports = router;
diff --git a/backend/utils/addUnknowns.js b/backend/utils/addUnknowns.js
index 571b8b1..19eea51 100644
--- a/backend/utils/addUnknowns.js
+++ b/backend/utils/addUnknowns.js
@@ -13,9 +13,6 @@ async function addUnknowns(locationData, occupiedCourts) {
         const isPhysicallyOccupied = occupiedCourts.includes(courtNumber);
         const isVirtuallyEmpty = activeFirebaseUIDs[i].startsWith('Empty');
 
-        // Reset FCM token to NULL
-        activeTokens[i] = 'NULL';
-
         // If court is physically occupied but virtually empty
         if (isPhysicallyOccupied && isVirtuallyEmpty) {
             const newName = `Unknown${courtNumber}`;
@@ -26,6 +23,7 @@ async function addUnknowns(locationData, occupiedCourts) {
             activeFirebaseUIDs[i] = newName;
             activeNicknames[i] = newName;
             activeStartTimes[i] = timestamp;
+            activeTokens[i] = 'NULL';
 
         // If court is physically empty but virtually occupied
         } else if (!isPhysicallyOccupied && !isVirtuallyEmpty) {
@@ -34,6 +32,7 @@ async function addUnknowns(locationData, occupiedCourts) {
             activeWaitingPlayers[i] = false;
             activeFirebaseUIDs[i] = newName;
             activeNicknames[i] = newName;
+            activeTokens[i] = 'NULL';
         }
     }
 }
diff --git a/backend/utils/advanceQueue.js b/backend/utils/advanceQueue.js
index 7375085..9a81469 100644
--- a/backend/utils/advanceQueue.js
+++ b/backend/utils/advanceQueue.js
@@ -1,5 +1,5 @@
 const dynamicBuffer = require('./dynamicBuffer'); // Import dynamicBuffer
-// const sendWebNotification = require('./sendWebNotification'); // Import sendWebNotification
+const sendWebNotification = require('./sendNotification'); // Import sendWebNotification
 const createTimestamps = require('./createTimestamps');
 const recordWaitTime = require('./recordWaitTime');
 
@@ -10,7 +10,8 @@ async function advanceQueue(locationData, location) {
     const timestamps = await createTimestamps(activeFirebaseUIDs.length);
     let lastTimestampIdx = 0;
 
-    queueAdvanced = false;
+    let queueAdvanced = false;
+
     for (let i = 0; i < activeFirebaseUIDs.length; i++) {
         
         // If there are no more players in the queue, return early (don't advance the queue further)
@@ -18,9 +19,9 @@ async function advanceQueue(locationData, location) {
             break;
         }
 
-        // If current court is empty, move first player in the current queue to active
+        // If the current court is empty, move the first player in the queue to active
         if (activeFirebaseUIDs[i].startsWith('Empty')) {
-            // Get UID and nickname of first player in the current queue
+            // Get UID and nickname of first player in the queue
             const firstQueuePlayerUID = queueFirebaseUIDs.shift();
             const firstQueuePlayerNickname = queueNicknames.shift();
             const firstQueueJoinTime = queueJoinTimes.shift();
@@ -33,7 +34,7 @@ async function advanceQueue(locationData, location) {
             activeWaitingPlayers[i] = false;
             activeTokens[i] = firstQueueToken;
 
-            // Record metrics once new player has joined court
+            // Record metrics once the new player has joined the court
             const now = new Date();
             const date = now.toLocaleDateString("en-CA", { year: "numeric", month: "2-digit", day: "2-digit" });
             recordWaitTime(location, date, firstQueueJoinTime, timestamps[lastTimestampIdx]);
@@ -42,11 +43,14 @@ async function advanceQueue(locationData, location) {
             queueAdvanced = true;
 
             // Notify the player that it's now their turn
-            // await sendWebNotification(firstQueuePlayerUID, `It\'s now your turn at court ${i + 1}!`);
+            await sendWebNotification(location, firstQueuePlayerUID, {
+                title: "Your Turn!",
+                body: `It's now your turn at court ${i + 1}!`
+            });
         }
     }
 
-    // If queue was not advanced, return early (no need to call dynamicBuffer and sendWebNotification)
+    // If queue was not advanced, return early (no need to call dynamicBuffer or notify players)
     if (!queueAdvanced) {
         return 204;  // No need for update as courts occupied or queue empty
     }
@@ -54,12 +58,16 @@ async function advanceQueue(locationData, location) {
     // Call dynamicBuffer
     await dynamicBuffer(locationData);
 
-    // If queue is not empty, notify next player that they are now first in line
-    // if (queueFirebaseUIDs.length > 0) {
-    //     const nextPlayerUID = queueFirebaseUIDs[0];
-    //     await sendWebNotification(nextPlayerUID, 'You are now first in line!');
-    // }
+    // Notify the next player in the queue (if any) that they are now first in line
+    if (queueFirebaseUIDs.length > 0) {
+        const nextPlayerUID = queueFirebaseUIDs[0];
+        await sendWebNotification(location, nextPlayerUID, {
+            title: "You're Next!",
+            body: "You are now first in line! Get ready for your turn."
+        });
+    }
+
     return 200;
 }
 
-module.exports = advanceQueue;
\ No newline at end of file
+module.exports = advanceQueue;
diff --git a/backend/utils/dynamicBuffer.js b/backend/utils/dynamicBuffer.js
index 728f0c7..23320d8 100644
--- a/backend/utils/dynamicBuffer.js
+++ b/backend/utils/dynamicBuffer.js
@@ -1,9 +1,7 @@
-// const sendWebNotification = require('./sendWebNotification'); // Import sendWebNotification
-// const scheduleEndSession = require('./scheduleEndSession'); // Import scheduleEndSession
+const sendWebNotification = require('./sendNotification'); // Import sendWebNotification
 
-async function dynamicBuffer(locationData) {
-
-    const { activeFirebaseUIDs, activeNicknames, activeStartTimes, activeWaitingPlayers, queueFirebaseUIDs, queueNicknames, queueJoinTimes} = locationData;
+async function dynamicBuffer(locationData, location) {
+    const { activeFirebaseUIDs, activeNicknames, activeStartTimes, activeWaitingPlayers, queueFirebaseUIDs, queueNicknames, queueJoinTimes } = locationData;
 
     // Count how many active players have someone waiting
     const numberWaitingPlayers = activeWaitingPlayers.filter(value => value === true).length;
@@ -64,8 +62,11 @@ async function dynamicBuffer(locationData) {
         }
         activeNicknames[player.index] += ` [${formattedTime}]`;
 
-        // Uncomment these once implemented
-        // sendWebNotification(player.firebaseUID, bufferTime);
+        // 📢 Immediately notify the player about their remaining time
+        await sendWebNotification(location, player.firebaseUID, {
+            title: "Time Update",
+            body: `You have ${bufferTime} minutes left on court ${player.index + 1}.`
+        });
     }
 }
 
diff --git a/backend/utils/endSession.js b/backend/utils/endSession.js
index 25cc30f..2591f11 100644
--- a/backend/utils/endSession.js
+++ b/backend/utils/endSession.js
@@ -1,5 +1,5 @@
 async function endSession(locationData, firebaseUID) {
-    const { activeFirebaseUIDs, activeNicknames, activeWaitingPlayers } = locationData;
+    const { activeFirebaseUIDs, activeNicknames, activeWaitingPlayers, activeTokens } = locationData;
 
     // Check whether the player exists
     const playerIndex = activeFirebaseUIDs.indexOf(firebaseUID);
@@ -12,6 +12,7 @@ async function endSession(locationData, firebaseUID) {
     activeFirebaseUIDs[playerIndex] = newName;
     activeNicknames[playerIndex] = newName;
     activeWaitingPlayers[playerIndex] = false;
+    activeTokens[playerIndex] = 'NULL';
     return 200;
 }
 
diff --git a/backend/utils/sendNotification.js b/backend/utils/sendNotification.js
new file mode 100644
index 0000000..87ba805
--- /dev/null
+++ b/backend/utils/sendNotification.js
@@ -0,0 +1,86 @@
+const admin = require('firebase-admin');
+
+/**
+ * Sends a web push notification to a user based on their UID.
+ * @param {string} location - The location ID in Firestore.
+ * @param {string} uid - The user's Firebase UID.
+ * @param {object} message - The message object containing title and body.
+ * @returns {Promise<object>} - A response object with the notification result.
+ */
+async function sendWebNotification(location, uid, message) {
+  try {
+    // Validate input parameters
+    if (!location || !uid || !message || !message.title || !message.body) {
+      throw new Error("Location, UID, and message (with title and body) are required");
+    }
+
+    // Retrieve the 'location' document from Firestore
+    const locationRef = admin.firestore().collection('locations').doc(location);
+    const locationSnapshot = await locationRef.get();
+
+    // Check if the location exists
+    if (!locationSnapshot.exists) {
+      throw new Error("Location not found.");
+    }
+
+    const locationData = locationSnapshot.data();
+
+    // Find the user in the queue or active players list
+    let index = locationData.queueFirebaseUIDs.indexOf(uid);
+    let fcmToken = null;
+    let userSource = 'queue';
+
+    if (index === -1) {
+      index = locationData.activeFirebaseUIDs.indexOf(uid);
+      userSource = 'active';
+
+      if (index === -1) {
+        throw new Error(`User UID not found in queue or active players: ${uid}`);
+      }
+
+      fcmToken = locationData.activeTokens[index];
+    } else {
+      fcmToken = locationData.queueTokens[index];
+    }
+
+    // If FCM token is not found
+    if (!fcmToken) {
+      throw new Error(`FCM token not found for user: ${uid} in ${userSource}`);
+    }
+
+    console.log(`Sending notification to token [${userSource}]: ${fcmToken}`);
+
+    // Build the notification payload
+    const payload = {
+      notification: {
+        title: message.title,
+        body: message.body,
+      },
+      data: {
+        location,
+        uid,
+        userSource,
+      },
+      token: fcmToken,
+    };
+
+    // Send the notification
+    const response = await admin.messaging().send(payload);
+    console.log("Notification Response:", response);
+
+    return {
+      success: true,
+      message: "Notification sent successfully",
+      response,
+    };
+  } catch (error) {
+    console.error("Failed to send notification:", error);
+    return {
+      success: false,
+      message: error.message || "Server error",
+      error,
+    };
+  }
+}
+
+module.exports = sendWebNotification;
diff --git a/frontend/firebase-messaging-sw.js b/frontend/firebase-messaging-sw.js
index 2d64303..e1a9d6d 100644
--- a/frontend/firebase-messaging-sw.js
+++ b/frontend/firebase-messaging-sw.js
@@ -1,8 +1,43 @@
-if ('serviceWorker' in navigator) {
-  navigator.serviceWorker.register('../firebase-messaging-sw.js')
-    .then(function (registration) {
-      console.log('Registration successful, scope is:', registration.scope);
-    }).catch(function (err) {
-      console.log('Service worker registration failed, error:', err);
+// public/firebase-messaging-sw.js
+
+/* 
+  Using the "compat" importScripts approach, which is simplest for 
+  a plain service worker file in public/ 
+*/
+importScripts('https://www.gstatic.com/firebasejs/9.17.1/firebase-app-compat.js');
+importScripts('https://www.gstatic.com/firebasejs/9.17.1/firebase-messaging-compat.js');
+
+// 1) Your same Firebase config here:
+firebase.initializeApp({
+  apiKey: "AIzaSyDJd2dK...",
+  authDomain: "brampton-tennis-queue.firebaseapp.com",
+  projectId: "brampton-tennis-queue",
+  storageBucket: "brampton-tennis-queue.appspot.com",
+  messagingSenderId: "578763495949",
+  appId: "1:578763495949:web:5c83ba102bcf0e719a9344",
+});
+
+// 2) Get messaging instance for the SW
+const messaging = firebase.messaging();
+
+// 3) Background message handler: This fires in the service worker
+//    when a push message arrives and the page is not in focus or closed.
+messaging.onBackgroundMessage(async (payload) => {
+
+  // 2) Retrieve all open client pages/tabs for this origin.
+  //    "includeUncontrolled: true" means it will match pages
+  //    not yet fully controlled by the service worker as well.
+  const allClients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
+
+  // 3) For each open page, send a postMessage event containing the payload data.
+  //    The page can then listen for this event and show a custom alert or UI.
+  allClients.forEach((client) => {
+    client.postMessage({
+      type: 'SHOW_ALERT',
+      data: {
+        title: payload.notification?.title || 'Default Title',
+        body:  payload.notification?.body || 'Default Body'
+      }
     });
-}
\ No newline at end of file
+  });
+});
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 82f8893..f636227 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,27 +1,59 @@
+// src/App.tsx
+import { useState, useEffect } from 'react';
 import { Routes, Route } from 'react-router-dom';
+
 import NotFound from './pages/NotFound';
-import LocationSelection from './pages/LocationSelection/LocationSelection'
+import LocationSelection from './pages/LocationSelection/LocationSelection';
 import UserInfo from './pages/UserInfo/UserInfoForm';
 import SignIn from './pages/SignIn/SignIn';
 import MessagingPermission from './pages/MessagingPermission/MessagingPermission';
 import ActiveView from './pages/ActiveView/ActiveView';
 import JoinCourt from './pages/JoinCourt/JoinCourt';
 
-// Import contexts
 import { LocalStorageProvider } from './context/LocalStorageContext';
 import { AuthProvider } from './context/AuthContext';
-import ProtectedRoute from './components/ProtectedRoute.tsx';
+import ProtectedRoute from './components/ProtectedRoute';
+import MyCustomAlert from './components/Notifications/NotificationAlert';
 
 function App() {
+  // State to hold the alert data
+  const [alertVisible, setAlertVisible] = useState(false);
+  const [alertData, setAlertData] = useState({ title: '', body: '' });
+
+  useEffect(() => {
+    // 1) Register the SW if not done elsewhere
+    if ('serviceWorker' in navigator) {
+      navigator.serviceWorker
+        .register('/firebase-messaging-sw.js') 
+        .then((registration) => {
+          console.log('Service Worker registered:', registration.scope);
+        })
+        .catch((err) => {
+          console.error('SW registration failed:', err);
+        });
+      
+      // 2) Listen for messages from the SW
+      navigator.serviceWorker.addEventListener('message', (event) => {
+        if (event.data?.type === 'SHOW_ALERT') {
+          const { title, body } = event.data.data;
+          // Instead of alert(`[${title}] ${body}`);
+          // We store in state and show a custom modal
+          setAlertData({ title, body });
+          setAlertVisible(true);
+
+          // Optionally log to console, too
+          console.log(`[Background Message] ${title}: ${body}`);
+        }
+      });
+    }
+  }, []);
 
   return (
-    <LocalStorageProvider> 
+    <LocalStorageProvider>
       <AuthProvider>
-        {/* Navigation Links */}
         <nav>
+          {/* your nav links or nothing */}
         </nav>
-
-        {/* Define Routes */}
         <Routes>
           <Route path="*" element={<NotFound />} />
           <Route path="/" element={<LocationSelection />} />
@@ -35,8 +67,17 @@ function App() {
               <ProtectedRoute>
                 <ActiveView />
               </ProtectedRoute>
-            } 
-          />        </Routes>
+            }
+          />
+        </Routes>
+
+        {/* Render our custom alert at the very bottom, so it's on top */}
+        <MyCustomAlert
+          show={alertVisible}
+          title={alertData.title}
+          body={alertData.body}
+          onClose={() => setAlertVisible(false)}
+        />
       </AuthProvider>
     </LocalStorageProvider>
   );
diff --git a/frontend/src/components/Notifications/NotificationAlert.css b/frontend/src/components/Notifications/NotificationAlert.css
new file mode 100644
index 0000000..7bb3cf2
--- /dev/null
+++ b/frontend/src/components/Notifications/NotificationAlert.css
@@ -0,0 +1,53 @@
+/* Full-screen overlay that dims the background */
+.alert-overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.5);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 9999; 
+  }
+  
+  /* The alert "card" itself */
+  .alert-box {
+    background: #fff;
+    color: #333;
+    padding: 1rem 1.5rem;
+    border-radius: 8px;
+    max-width: 400px;
+    text-align: center;
+    box-shadow: 0 2px 10px rgba(0,0,0,0.3);
+  }
+  
+  /* Title styling */
+  .alert-title {
+    margin: 0 0 0.5rem 0;
+    font-size: 1.25rem;
+    font-weight: bold;
+  }
+  
+  /* Body text styling */
+  .alert-body {
+    margin: 0.5rem 0;
+    font-size: 1rem;
+  }
+  
+  /* Close button styling */
+  .alert-close {
+    margin-top: 1rem;
+    padding: 0.5rem 1rem;
+    background: #007aff;
+    color: #fff;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+  }
+  
+  .alert-close:hover {
+    opacity: 0.9;
+  }
+  
\ No newline at end of file
diff --git a/frontend/src/components/Notifications/NotificationAlert.tsx b/frontend/src/components/Notifications/NotificationAlert.tsx
new file mode 100644
index 0000000..398bc8f
--- /dev/null
+++ b/frontend/src/components/Notifications/NotificationAlert.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import './NotificationAlert.css'; // We'll define styles below
+
+interface MyCustomAlertProps {
+  show: boolean;
+  title: string;
+  body: string;
+  onClose: () => void;
+}
+
+/**
+ * MyCustomAlert
+ * A simple modal-like component that renders an overlay and a dialog box
+ * when 'show' is true. It displays a title, body text, and a "Close" button.
+ */
+const MyCustomAlert: React.FC<MyCustomAlertProps> = ({ show, title, body, onClose }) => {
+  if (!show) return null;
+
+  return (
+    <div className="alert-overlay">
+      <div className="alert-box">
+        <h3 className="alert-title">{title}</h3>
+        <p className="alert-body">{body}</p>
+        <button className="alert-close" onClick={onClose}>
+          Close
+        </button>
+      </div>
+    </div>
+  );
+};
+
+export default MyCustomAlert;
diff --git a/frontend/src/pages/ActiveView/RectangleWithOptions.tsx b/frontend/src/pages/ActiveView/RectangleWithOptions.tsx
index 67bdc8c..f5d27c4 100644
--- a/frontend/src/pages/ActiveView/RectangleWithOptions.tsx
+++ b/frontend/src/pages/ActiveView/RectangleWithOptions.tsx
@@ -132,9 +132,14 @@ const RectangleWithOptions: React.FC<RectangleWithOptionsProps> = ({
         await endSession(location, firebaseUID);  // Force others to end their session
         alert("Player session ended.");
       } else if (actionToConfirm === ACTIONS.SEND_WEB_REMINDER) {
-        await sendWebNotification(location, firebaseUID, "Time is up! Please end your session.");
-        alert("Sent reminder to player.")
-      }
+        // ✅ Corrected API Call to Send Reminder
+        await sendWebNotification(location, firebaseUID, {
+            title: "Reminder: Time’s Up!",
+            body: "Your court time has ended. Please make way for the next player."
+        });
+
+        alert("Reminder sent to player.");
+    }
 
       // Close the modal first
       setShowModal(false);  
diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js
index ef75f65..0bd0bd5 100644
--- a/frontend/src/utils/api.js
+++ b/frontend/src/utils/api.js
@@ -1,8 +1,7 @@
 import { db } from '../firebase';
 import { doc, onSnapshot } from 'firebase/firestore';
 
-const BACKEND_API = 'http://localhost:5001';
-
+const BACKEND_API = 'http://localhost:5001'; // <-- Update if needed
 
 // ** UTILITY FUNCTIONS TO CALL BACKEND ENDPOINTS **
 
@@ -14,9 +13,7 @@ export const fetchCurrentState = async (locationName, retries = 3, delay = 500)
       headers: {
         'Content-Type': 'application/json',
       },
-      body: JSON.stringify({
-        location: locationName,
-      }),
+      body: JSON.stringify({ location: locationName }),
     });
     if (!response.ok) throw new Error('Failed to retrieve current state.');
     const data = await response.json();
@@ -24,10 +21,10 @@ export const fetchCurrentState = async (locationName, retries = 3, delay = 500)
   } catch (error) {
     if (retries > 0) {
       console.log(`Retrying fetchCurrentState (${retries} retries left)...`);
-      await new Promise(res => setTimeout(res, delay));  // Exponential backoff
-      return await fetchCurrentState(locationName, retries - 1, delay * 2);  // Up to 3 retries
+      await new Promise(res => setTimeout(res, delay));
+      return await fetchCurrentState(locationName, retries - 1, delay * 2);
     }
-    console.error("Error fetching current state:", error);
+    console.error('Error fetching current state:', error);
     return null;
   }
 };
@@ -40,10 +37,7 @@ export const endSession = async (locationName, firebaseUID, retries = 3, delay =
       headers: {
         'Content-Type': 'application/json',
       },
-      body: JSON.stringify({
-        location: locationName,
-        firebaseUID: firebaseUID,
-      }),
+      body: JSON.stringify({ location: locationName, firebaseUID: firebaseUID }),
     });
     if (!response.ok) throw new Error('Failed to end session.');
     const data = await response.json();
@@ -51,10 +45,10 @@ export const endSession = async (locationName, firebaseUID, retries = 3, delay =
   } catch (error) {
     if (retries > 0) {
       console.log(`Retrying endSession (${retries} retries left)...`);
-      await new Promise(res => setTimeout(res, delay));  // Exponential backoff
-      return await endSession(locationName, firebaseUID, retries - 1, delay * 2);  // Up to 3 retries
+      await new Promise(res => setTimeout(res, delay));
+      return await endSession(locationName, firebaseUID, retries - 1, delay * 2);
     }
-    console.error("Error ending session:", error);
+    console.error('Error ending session:', error);
     return null;
   }
 };
@@ -79,11 +73,11 @@ export const joinGame = async (locationName, nickname, firebaseUID, token, retri
     return data;
   } catch (error) {
     if (retries > 0) {
-      console.log(`Retrying join game (${retries} retries left)...`);
-      await new Promise(res => setTimeout(res, delay));  // Exponential backoff
-      return await joinGame(locationName, nickname, firebaseUID, retries - 1, delay * 2);  // Up to 3 retries
+      console.log(`Retrying joinGame (${retries} retries left)...`);
+      await new Promise(res => setTimeout(res, delay));
+      return await joinGame(locationName, nickname, firebaseUID, retries - 1, delay * 2);
     }
-    console.error("Error joining game:", error);
+    console.error('Error joining game:', error);
     return null;
   }
 };
@@ -96,10 +90,7 @@ export const leaveQueue = async (locationName, firebaseUID, retries = 3, delay =
       headers: {
         'Content-Type': 'application/json',
       },
-      body: JSON.stringify({
-        location: locationName,
-        firebaseUID: firebaseUID,
-      }),
+      body: JSON.stringify({ location: locationName, firebaseUID: firebaseUID }),
     });
     if (!response.ok) throw new Error('Failed to leave queue.');
     const data = await response.json();
@@ -107,17 +98,19 @@ export const leaveQueue = async (locationName, firebaseUID, retries = 3, delay =
   } catch (error) {
     if (retries > 0) {
       console.log(`Retrying leaveQueue (${retries} retries left)...`);
-      await new Promise(res => setTimeout(res, delay));  // Exponential backoff
-      return await leaveQueue(locationName, firebaseUID, retries - 1, delay * 2);  // Up to 3 retries
+      await new Promise(res => setTimeout(res, delay));
+      return await leaveQueue(locationName, firebaseUID, retries - 1, delay * 2);
     }
-    console.error("Error leaving queue:", error);
+    console.error('Error leaving queue:', error);
     return null;
   }
 };
 
-
-// Send a reminder to an active player with the /sendWebNotification  endpoint
-export const sendWebNotification = async (locationName, firebaseUID, message, retries = 3, delay = 500) => {
+// ** New: Send a reminder/notification to a user with /sendWebNotification endpoint **
+export const sendWebNotification = async (locationName, uid, message, retries = 3, delay = 500) => {
+  console.log("locationName", locationName);
+  console.log("uid", uid);
+  console.log("message", message);
   try {
     const response = await fetch(`${BACKEND_API}/api/sendWebNotification`, {
       method: 'POST',
@@ -126,24 +119,26 @@ export const sendWebNotification = async (locationName, firebaseUID, message, re
       },
       body: JSON.stringify({
         location: locationName,
-        firebaseUID: firebaseUID,
-        message: message,
+        uid,
+        message, // message should be an object: { title: '...', body: '...' }
       }),
     });
+
     if (!response.ok) throw new Error('Failed to send web notification.');
     const data = await response.json();
     return data;
   } catch (error) {
     if (retries > 0) {
       console.log(`Retrying sendWebNotification (${retries} retries left)...`);
-      await new Promise(res => setTimeout(res, delay));  // Exponential backoff
-      return await sendWebNotification(locationName, firebaseUID, message, retries - 1, delay * 2);  // Up to 3 retries
+      await new Promise(res => setTimeout(res, delay));
+      return await sendWebNotification(locationName, uid, message, retries - 1, delay * 2);
     }
-    console.error("Error sending web notification:", error);
+    console.error('Error sending web notification:', error);
     return null;
   }
 };
 
+// Retrieve taken courts info with /getTaken endpoint
 export const getTaken = async (locationName, retries = 3, delay = 500) => {
   try {
     const response = await fetch(`${BACKEND_API}/api/getTaken`, {
@@ -151,9 +146,7 @@ export const getTaken = async (locationName, retries = 3, delay = 500) => {
       headers: {
         'Content-Type': 'application/json',
       },
-      body: JSON.stringify({
-        location: locationName,
-      }),
+      body: JSON.stringify({ location: locationName }),
     });
     if (!response.ok) throw new Error('Failed to retrieve taken courts.');
     const data = await response.json();
@@ -162,14 +155,15 @@ export const getTaken = async (locationName, retries = 3, delay = 500) => {
   } catch (error) {
     if (retries > 0) {
       console.log(`Retrying getTaken (${retries} retries left)...`);
-      await new Promise(res => setTimeout(res, delay)); // Exponential backoff
-      return await getTaken(locationName, retries - 1, delay * 2); // Retry logic
+      await new Promise(res => setTimeout(res, delay));
+      return await getTaken(locationName, retries - 1, delay * 2);
     }
-    console.error("Error fetching taken courts:", error);
+    console.error('Error fetching taken courts:', error);
     return null;
   }
 };
 
+// Add unknown players to the location with /addUnknowns endpoint
 export const addUnknowns = async (locationName, courts, retries = 3, delay = 500) => {
   try {
     const response = await fetch(`${BACKEND_API}/api/addUnknowns`, {
@@ -188,17 +182,15 @@ export const addUnknowns = async (locationName, courts, retries = 3, delay = 500
   } catch (error) {
     if (retries > 0) {
       console.log(`Retrying addUnknowns (${retries} retries left)...`);
-      await new Promise(res => setTimeout(res, delay)); // Exponential backoff
-      return await addUnknowns(locationName, courts, retries - 1, delay * 2); // Retry logic
+      await new Promise(res => setTimeout(res, delay));
+      return await addUnknowns(locationName, courts, retries - 1, delay * 2);
     }
-    console.error("Error adding unknowns:", error);
+    console.error('Error adding unknowns:', error);
     return null;
   }
 };
 
 // ** UTILITY FUNCTION TO CREATE FIRESTORE LISTENER **
-
-// Subscribe to Firestore snapshot for a specific location document
 export const subscribeToLocation = (location, updateState) => {
   // Create reference to location document
   const locationRef = doc(db, 'locations', location);
@@ -233,20 +225,18 @@ export const expectedWaitTime = async (locationName, retries = 3, delay = 500) =
       headers: {
         'Content-Type': 'application/json',
       },
-      body: JSON.stringify({
-        location: locationName,
-      }),
+      body: JSON.stringify({ location: locationName }),
     });
-    if (!response.ok) throw new Error('Failed to leave queue.');
+    if (!response.ok) throw new Error('Failed to fetch expected wait time.');
     const data = await response.json();
     return data;
   } catch (error) {
     if (retries > 0) {
       console.log(`Retrying expectedWaitTime (${retries} retries left)...`);
-      await new Promise(res => setTimeout(res, delay));  // Exponential backoff
-      return await expectedWaitTime(locationName, retries - 1, delay * 2);  // Up to 3 retries
+      await new Promise(res => setTimeout(res, delay));
+      return await expectedWaitTime(locationName, retries - 1, delay * 2);
     }
     console.error(`Error getting expected wait time for location ${locationName}: ${error}`);
     return null;
   }
-};
\ No newline at end of file
+};


Reviews:
user: login: Ron-Madan
body: Hey Divyansh, great work! Also, can you please modify the /advanceQueue and /dynamicBuffer backend so they can trigger messages using the new endpoint? Thanks!
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T19:08:38Z
user: login: Ron-Madan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T18:07:30Z
user: login: Ron-Madan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T18:17:52Z
user: login: Divyanshuoft
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-08T07:07:35Z
user: login: Divyanshuoft
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-08T07:11:44Z
user: login: Ron-Madan
body: LGTM!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T17:09:15Z

Review comments:
diff_hunk: @@ -0,0 +1,101 @@
+const express = require('express');
+const router = express.Router();
+const admin = require('firebase-admin');
+const sendWebNotification = require('../utils/sendNotification');  // Import the add unknowns utility
+
+/**
+ * POST /sendWebNotification
+ *
+ * Sends a web push notification to a user (identified by `uid`) who might be
+ * in a queue or an activePlayers list at a given location. The message includes
+ * both `notification` and `data` fields for Firebase Cloud Messaging.
+ */
+router.post('/sendWebNotification', async (req, res) => {
+  try {
+    // 1) Extract relevant fields from request body
+    const { location, uid, message } = req.body;
+
+    // 2) Validate that required fields are present
+    if (!location || !uid || !message || !message.title || !message.body) {
+      return res.status(400).json({
+        message: "Location, UID, and message (with title and body) are required",
+      });
path: backend/routes/sendWebNotificationRoute.js
user: login: Ron-Madan
body: Hey Divyansh, hope you're well! Can we add this commented code inside the utility and call the utility here as you did in the advanceQueue and dynamicBuffer utilities? Thanks!
created_at: 2025-03-06T18:07:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,8 +62,16 @@ async function dynamicBuffer(locationData) {
         }
         activeNicknames[player.index] += ` [${formattedTime}]`;
 
-        // Uncomment these once implemented
-        // sendWebNotification(player.firebaseUID, bufferTime);
+        // Schedule a notification 5 minutes before time is up
+        const timeUntilNotification = (bufferTime - 5) * 60 * 1000; // Convert to milliseconds
+        if (timeUntilNotification > 0) {
+            setTimeout(async () => {
+                await sendWebNotification(location, player.firebaseUID, {
+                    title: "Time Almost Up!",
+                    body: `You have 5 minutes left on court ${player.index + 1}.`
+                });
path: backend/utils/dynamicBuffer.js
user: login: Ron-Madan
body: Hey Divyansh, in order to avoid holding the backend with the timeout, can we instead just send them a message saying you have x minutes left? Thanks!
created_at: 2025-03-06T18:17:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,8 +62,16 @@ async function dynamicBuffer(locationData) {
         }
         activeNicknames[player.index] += ` [${formattedTime}]`;
 
-        // Uncomment these once implemented
-        // sendWebNotification(player.firebaseUID, bufferTime);
+        // Schedule a notification 5 minutes before time is up
+        const timeUntilNotification = (bufferTime - 5) * 60 * 1000; // Convert to milliseconds
+        if (timeUntilNotification > 0) {
+            setTimeout(async () => {
+                await sendWebNotification(location, player.firebaseUID, {
+                    title: "Time Almost Up!",
+                    body: `You have 5 minutes left on court ${player.index + 1}.`
+                });
path: backend/utils/dynamicBuffer.js
user: login: Divyanshuoft
body: in the new commit, now i don't use timeout instead now I send the message directly...
created_at: 2025-03-08T07:07:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+const express = require('express');
+const router = express.Router();
+const admin = require('firebase-admin');
+const sendWebNotification = require('../utils/sendNotification');  // Import the add unknowns utility
+
+/**
+ * POST /sendWebNotification
+ *
+ * Sends a web push notification to a user (identified by `uid`) who might be
+ * in a queue or an activePlayers list at a given location. The message includes
+ * both `notification` and `data` fields for Firebase Cloud Messaging.
+ */
+router.post('/sendWebNotification', async (req, res) => {
+  try {
+    // 1) Extract relevant fields from request body
+    const { location, uid, message } = req.body;
+
+    // 2) Validate that required fields are present
+    if (!location || !uid || !message || !message.title || !message.body) {
+      return res.status(400).json({
+        message: "Location, UID, and message (with title and body) are required",
+      });
path: backend/routes/sendWebNotificationRoute.js
user: login: Divyanshuoft
body: yep, now using utility in the route to make things simpler in the new commit
created_at: 2025-03-08T07:11:44Z
author_association: CONTRIBUTOR

PR comments:
user: login: Divyanshuoft
created_at: 2025-03-06T02:54:20Z
body: > Hey Divyansh, great work! Also, can you please modify the /advanceQueue and /dynamicBuffer backend so they can trigger messages using the new endpoint? Thanks!

@Ron-Madan Modified advanceQueue, dynamicBuffer to call to send notification messages :) in the new commit plz 
author_association: CONTRIBUTOR
user: login: Ron-Madan
created_at: 2025-03-08T19:43:02Z
body: Hey Divyansh, thanks for the prompt changes!

I just remembered that the "Send Reminder" button on the frontend does not currently call this endpoint.

Can you import the API in the utility file and modify the rectangle button so it calls /sendWebNotification?

I apologize for the repeated changes...
author_association: CONTRIBUTOR
user: login: Divyanshuoft
created_at: 2025-03-10T17:01:53Z
body: > Hey Divyansh, thanks for the prompt changes!
> 
> I just remembered that the "Send Reminder" button on the frontend does not currently call this endpoint.
> 
> Can you import the API in the utility file and modify the rectangle button so it calls /sendWebNotification?
> 
> I apologize for the repeated changes...

updated the code for send reminder, please see the attached screenshots for more clarity
author_association: CONTRIBUTOR