@@ -3,14 +3,25 @@ import React, { useState, useEffect, useCallback, useRef } from "react"
33import { usePathname , useRouter } from "next/navigation"
44import { AnimatePresence } from "framer-motion"
55import NotificationsOverlay from "@components/NotificationsOverlay"
6- import { IconMenu2 , IconLoader } from "@tabler/icons-react"
6+ import { IconMenu2 , IconLoader , IconX } from "@tabler/icons-react"
77import Sidebar from "@components/Sidebar"
88import CommandPalette from "./CommandPallete"
99import GlobalSearch from "./GlobalSearch"
1010import { useGlobalShortcuts } from "@hooks/useGlobalShortcuts"
1111import { cn } from "@utils/cn"
1212import toast from "react-hot-toast"
1313
14+ // Helper function to convert VAPID key
15+ function urlBase64ToUint8Array ( base64String ) {
16+ const padding = "=" . repeat ( ( 4 - ( base64String . length % 4 ) ) % 4 ) ;
17+ const base64 = ( base64String + padding ) . replace ( / - / g, "+" ) . replace ( / _ / g, "/" ) ;
18+ const rawData = window . atob ( base64 ) ;
19+ const outputArray = new Uint8Array ( rawData . length ) ;
20+ for ( let i = 0 ; i < rawData . length ; ++ i ) {
21+ outputArray [ i ] = rawData . charCodeAt ( i ) ;
22+ }
23+ return outputArray ;
24+ }
1425export default function LayoutWrapper ( { children } ) {
1526 const [ isNotificationsOpen , setNotificationsOpen ] = useState ( false )
1627 const [ isSearchOpen , setSearchOpen ] = useState ( false )
@@ -99,7 +110,36 @@ export default function LayoutWrapper({ children }) {
99110 )
100111 ws . onmessage = ( event ) => {
101112 const data = JSON . parse ( event . data )
102- if ( data . type === "task_progress_update" ) {
113+ if ( data . type === "new_notification" ) {
114+ setUnreadCount ( ( prev ) => prev + 1 )
115+ toast (
116+ ( t ) => (
117+ < div className = "flex items-center gap-3" >
118+ < span className = "flex-1" >
119+ { data . notification . message }
120+ </ span >
121+ < button
122+ onClick = { ( ) => {
123+ handleNotificationsOpen ( )
124+ toast . dismiss ( t . id )
125+ } }
126+ className = "py-1 px-3 rounded-md bg-brand-orange text-black text-sm font-semibold"
127+ >
128+ View
129+ </ button >
130+ < button
131+ onClick = { ( ) => toast . dismiss ( t . id ) }
132+ className = "p-1.5 rounded-full hover:bg-neutral-700"
133+ >
134+ < IconX size = { 16 } />
135+ </ button >
136+ </ div >
137+ ) ,
138+ {
139+ duration : 6000
140+ }
141+ )
142+ } else if ( data . type === "task_progress_update" ) {
103143 // Dispatch a custom event that the tasks page can listen for
104144 window . dispatchEvent (
105145 new CustomEvent ( "taskProgressUpdate" , {
@@ -146,6 +186,33 @@ export default function LayoutWrapper({ children }) {
146186 setCommandPaletteOpen ( ( prev ) => ! prev )
147187 )
148188
189+ // PWA Update Handler
190+
191+ useEffect ( ( ) => {
192+ // This effect runs only on the client side, after the component mounts.
193+ if (
194+ "serviceWorker" in navigator &&
195+ process . env . NODE_ENV !== "development"
196+ ) {
197+ // The 'load' event ensures that SW registration doesn't delay page rendering.
198+ window . addEventListener ( "load" , function ( ) {
199+ navigator . serviceWorker . register ( "/sw.js" ) . then (
200+ function ( registration ) {
201+ console . log (
202+ "ServiceWorker registration successful with scope: " ,
203+ registration . scope
204+ )
205+ } ,
206+ function ( err ) {
207+ console . log ( "ServiceWorker registration failed: " , err )
208+ }
209+ )
210+ } )
211+ }
212+ } , [ ] )
213+
214+ // Removed duplicate subscribeToPushNotifications declaration
215+
149216 // PWA Update Handler
150217 useEffect ( ( ) => {
151218 if (
@@ -221,16 +288,54 @@ export default function LayoutWrapper({ children }) {
221288 }
222289 } , [ ] )
223290
224- useEffect ( ( ) => {
225- const handleEscape = ( e ) => {
226- if ( e . key === "Escape" ) {
227- if ( isNotificationsOpen ) setNotificationsOpen ( false )
228- if ( isCommandPaletteOpen ) setCommandPaletteOpen ( false )
291+ const subscribeToPushNotifications = useCallback ( async ( ) => {
292+ if ( ! ( "serviceWorker" in navigator ) || ! ( "PushManager" in window ) ) return ;
293+ if ( ! process . env . NEXT_PUBLIC_VAPID_PUBLIC_KEY ) {
294+ console . warn ( "VAPID public key not configured. Skipping push subscription." ) ;
295+ return ;
296+ }
297+ console . log ( "VAPID Public Key is configured. Proceeding with push subscription." ) ;
298+
299+ try {
300+ const registration = await navigator . serviceWorker . ready ;
301+ console . log ( "Service Worker is ready:" , registration ) ;
302+
303+ let subscription = await registration . pushManager . getSubscription ( ) ;
304+ console . log ( "Existing subscription:" , subscription ) ;
305+
306+ if ( subscription === null ) {
307+ console . log ( "No existing subscription found. Requesting permission..." ) ;
308+ const permission = await window . Notification . requestPermission ( ) ;
309+ console . log ( "Notification permission status:" , permission ) ;
310+
311+ if ( permission !== "granted" ) return ;
312+
313+ console . log ( "Permission granted. Subscribing to push manager..." ) ;
314+ subscription = await registration . pushManager . subscribe ( {
315+ userVisibleOnly : true ,
316+ applicationServerKey : urlBase64ToUint8Array ( process . env . NEXT_PUBLIC_VAPID_PUBLIC_KEY ) ,
317+ } ) ;
318+ console . log ( "New subscription created:" , subscription ) ;
319+
320+ const response = await fetch ( "/api/notifications/subscribe" , {
321+ method : "POST" ,
322+ headers : { "Content-Type" : "application/json" } ,
323+ body : JSON . stringify ( subscription ) ,
324+ } ) ;
325+
326+ if ( ! response . ok ) {
327+ throw new Error ( "Failed to send subscription to server." ) ;
328+ }
329+ console . log ( "Subscription successfully sent to server." ) ;
229330 }
331+ } catch ( error ) {
332+ console . error ( "Error during push notification subscription:" , error ) ;
230333 }
231- window . addEventListener ( "keydown" , handleEscape )
232- return ( ) => window . removeEventListener ( "keydown" , handleEscape )
233- } , [ isNotificationsOpen , isCommandPaletteOpen ] )
334+ } , [ ] ) ;
335+
336+ useEffect ( ( ) => {
337+ if ( showNav && userDetails ?. sub ) subscribeToPushNotifications ( )
338+ } , [ showNav , userDetails , subscribeToPushNotifications ] )
234339
235340 if ( isLoading ) {
236341 return (
@@ -301,3 +406,4 @@ export default function LayoutWrapper({ children }) {
301406 </ >
302407 )
303408}
409+
0 commit comments