Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect, useRef } from "react"; | |
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { Clock, Lock, Coffee, Users, Mail, PanelRight, Bell, X, BellOff, BellRing } from "lucide-react"; | |
| import { toast } from "sonner"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Switch } from "@/components/ui/switch"; | |
| interface ScheduleItem { | |
| time: string; | |
| activity: string; | |
| type: "focused" | "collaborative" | "rest" | "light"; | |
| icon: React.ReactNode; | |
| } | |
| export default function WorkSchedule() { | |
| const [currentTime, setCurrentTime] = useState<Date>(new Date()); | |
| const [currentActivityIndex, setCurrentActivityIndex] = useState<number>(-1); | |
| const [lastNotifiedIndex, setLastNotifiedIndex] = useState<number>(-1); | |
| const [showNotificationModal, setShowNotificationModal] = useState<boolean>(false); | |
| const [notificationsEnabled, setNotificationsEnabled] = useState<boolean>(true); | |
| const [browserNotificationsEnabled, setBrowserNotificationsEnabled] = useState<boolean>(false); | |
| const [browserNotificationsPermission, setBrowserNotificationsPermission] = useState<string>("default"); | |
| const [currentNotification, setCurrentNotification] = useState<{ | |
| title: string; | |
| description: string; | |
| type: "focused" | "collaborative" | "rest" | "light"; | |
| } | null>(null); | |
| const [audioInitialized, setAudioInitialized] = useState<boolean>(false); | |
| // Audio element ref for better control and preloading | |
| const notificationAudioRef = useRef<HTMLAudioElement | null>(null); | |
| // Initialize audio system on first user interaction | |
| const initializeAudio = () => { | |
| if (audioInitialized) return; | |
| console.log("Initializing audio system from user interaction"); | |
| try { | |
| // Try to play a silent sound to unlock audio | |
| const audioElement = document.getElementById('notification-sound-test') as HTMLAudioElement; | |
| if (audioElement) { | |
| audioElement.volume = 0.5; // Nearly silent | |
| const promise = audioElement.play(); | |
| if (promise !== undefined) { | |
| promise | |
| .then(() => { | |
| console.log("Audio system unlocked successfully"); | |
| // Pause immediately after unlocking | |
| audioElement.pause(); | |
| audioElement.currentTime = 0; | |
| setAudioInitialized(true); | |
| }) | |
| .catch(err => { | |
| console.error("Could not unlock audio system:", err); | |
| }); | |
| } | |
| } | |
| // Also try to use the ref-based audio | |
| if (notificationAudioRef.current) { | |
| notificationAudioRef.current.volume = 0.01; | |
| const promise = notificationAudioRef.current.play(); | |
| if (promise !== undefined) { | |
| promise | |
| .then(() => { | |
| console.log("Ref audio system unlocked successfully"); | |
| notificationAudioRef.current?.pause(); | |
| if (notificationAudioRef.current) { | |
| notificationAudioRef.current.currentTime = 0; | |
| } | |
| setAudioInitialized(true); | |
| }) | |
| .catch(err => { | |
| console.error("Could not unlock ref audio system:", err); | |
| }); | |
| } | |
| } | |
| } catch (error) { | |
| console.error("Error initializing audio:", error); | |
| } | |
| }; | |
| // Add global click listener to initialize audio on first interaction | |
| useEffect(() => { | |
| const handleUserInteraction = () => { | |
| initializeAudio(); | |
| // Remove listeners after initialization | |
| if (audioInitialized) { | |
| document.removeEventListener('click', handleUserInteraction); | |
| document.removeEventListener('touchstart', handleUserInteraction); | |
| document.removeEventListener('keydown', handleUserInteraction); | |
| } | |
| }; | |
| document.addEventListener('click', handleUserInteraction); | |
| document.addEventListener('touchstart', handleUserInteraction); | |
| document.addEventListener('keydown', handleUserInteraction); | |
| return () => { | |
| document.removeEventListener('click', handleUserInteraction); | |
| document.removeEventListener('touchstart', handleUserInteraction); | |
| document.removeEventListener('keydown', handleUserInteraction); | |
| }; | |
| }, [audioInitialized]); | |
| // Initialize audio element on component mount | |
| useEffect(() => { | |
| // Create audio element and preload it | |
| const audio = new Audio('/notification.mp3'); | |
| audio.preload = 'auto'; | |
| audio.volume = 1.0; | |
| // Log loading events for debugging | |
| audio.addEventListener('canplaythrough', () => { | |
| console.log("Notification sound loaded and ready to play"); | |
| }); | |
| audio.addEventListener('error', (e) => { | |
| console.error("Error loading notification sound:", e); | |
| // Try with an alternative sound file as fallback | |
| console.log("Trying fallback notification sound"); | |
| audio.src = '/notification-fallback.mp3'; | |
| // If that fails too, create a simple beep using the Web Audio API | |
| audio.addEventListener('error', () => { | |
| console.log("Creating synthetic beep as last resort"); | |
| try { | |
| // Create a simple beep using Web Audio API as last resort | |
| const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); | |
| const oscillator = audioContext.createOscillator(); | |
| const gainNode = audioContext.createGain(); | |
| oscillator.type = 'sine'; | |
| oscillator.frequency.value = 800; // frequency in hertz | |
| gainNode.gain.value = 0.5; | |
| oscillator.connect(gainNode); | |
| gainNode.connect(audioContext.destination); | |
| // Store the audio context for cleanup | |
| (window as any).scheduleAudioContext = audioContext; | |
| // Define a method to play this synthetic beep | |
| notificationAudioRef.current = { | |
| play: () => { | |
| oscillator.start(); | |
| oscillator.stop(audioContext.currentTime + 0.3); // beep for 300ms | |
| return Promise.resolve(); | |
| }, | |
| pause: () => {}, | |
| currentTime: 0 | |
| } as any; | |
| console.log("Synthetic beep created successfully"); | |
| } catch (err) { | |
| console.error("Failed to create synthetic beep:", err); | |
| } | |
| }); | |
| }); | |
| // Store the reference | |
| notificationAudioRef.current = audio; | |
| // Clean up on unmount | |
| return () => { | |
| if (notificationAudioRef.current) { | |
| if ('pause' in notificationAudioRef.current) { | |
| notificationAudioRef.current.pause(); | |
| } | |
| notificationAudioRef.current = null; | |
| } | |
| // Clean up audio context if it was created | |
| if ((window as any).scheduleAudioContext) { | |
| (window as any).scheduleAudioContext.close().catch(console.error); | |
| delete (window as any).scheduleAudioContext; | |
| } | |
| }; | |
| }, []); | |
| // Helper function to play notification sound | |
| const playNotificationSound = () => { | |
| if (!notificationAudioRef.current) { | |
| console.error("Audio element not initialized"); | |
| return; | |
| } | |
| console.log("Attempting to play notification sound"); | |
| // Try to initialize audio system if not already done | |
| if (!audioInitialized) { | |
| console.log("Audio not initialized, attempting to initialize first"); | |
| initializeAudio(); | |
| // Show a toast prompting user interaction | |
| toast.info("Click anywhere to enable sounds", { | |
| duration: 3000, | |
| action: { | |
| label: "Enable", | |
| onClick: initializeAudio | |
| } | |
| }); | |
| } | |
| try { | |
| // Reset audio to beginning in case it was already played | |
| notificationAudioRef.current.currentTime = 0; | |
| // Play the sound | |
| const playPromise = notificationAudioRef.current.play(); | |
| if (playPromise !== undefined) { | |
| playPromise | |
| .then(() => console.log("Audio played successfully")) | |
| .catch(err => { | |
| console.error("Could not play notification sound", err); | |
| // Try to play using the DOM audio element as fallback | |
| const audioElement = document.getElementById('notification-sound-test') as HTMLAudioElement; | |
| if (audioElement) { | |
| console.log("Trying to play via DOM audio element as fallback"); | |
| audioElement.currentTime = 0; | |
| audioElement.volume = 0.5; | |
| audioElement.play() | |
| .then(() => console.log("DOM audio played successfully as fallback")) | |
| .catch(domErr => { | |
| console.error("DOM audio fallback also failed:", domErr); | |
| // Most browsers require user interaction before playing audio | |
| // Create a one-time click event listener to try playing again | |
| const playOnNextClick = () => { | |
| initializeAudio(); | |
| document.removeEventListener('click', playOnNextClick); | |
| }; | |
| document.addEventListener('click', playOnNextClick, { once: true }); | |
| toast.info("Click anywhere to enable sound notifications", { | |
| duration: 3000, | |
| }); | |
| }); | |
| } | |
| }); | |
| } | |
| } catch (error) { | |
| console.error("Error playing notification sound:", error); | |
| } | |
| }; | |
| // Check if browser notifications are supported | |
| const browserNotificationsSupported = typeof window !== 'undefined' && 'Notification' in window; | |
| // Load notification preferences from localStorage on component mount | |
| useEffect(() => { | |
| const savedPreference = localStorage.getItem('workScheduleNotificationsEnabled'); | |
| if (savedPreference !== null) { | |
| const enabled = savedPreference === 'true'; | |
| console.log('Loading notification preference from localStorage:', enabled); | |
| setNotificationsEnabled(enabled); | |
| } | |
| const savedBrowserNotifPreference = localStorage.getItem('workScheduleBrowserNotificationsEnabled'); | |
| if (savedBrowserNotifPreference !== null) { | |
| const enabled = savedBrowserNotifPreference === 'true'; | |
| console.log('Loading browser notification preference from localStorage:', enabled); | |
| setBrowserNotificationsEnabled(enabled); | |
| } | |
| // Check current browser notification permission | |
| if (browserNotificationsSupported) { | |
| setBrowserNotificationsPermission(Notification.permission); | |
| } | |
| }, [browserNotificationsSupported]); | |
| // Save notification preferences to localStorage when changed | |
| useEffect(() => { | |
| console.log('Saving notification preference to localStorage:', notificationsEnabled); | |
| localStorage.setItem('workScheduleNotificationsEnabled', notificationsEnabled.toString()); | |
| }, [notificationsEnabled]); | |
| useEffect(() => { | |
| console.log('Saving browser notification preference to localStorage:', browserNotificationsEnabled); | |
| localStorage.setItem('workScheduleBrowserNotificationsEnabled', browserNotificationsEnabled.toString()); | |
| }, [browserNotificationsEnabled]); | |
| // Request browser notification permission | |
| const requestBrowserNotificationPermission = async () => { | |
| if (!browserNotificationsSupported) return; | |
| try { | |
| const permission = await Notification.requestPermission(); | |
| setBrowserNotificationsPermission(permission); | |
| if (permission === 'granted') { | |
| setBrowserNotificationsEnabled(true); | |
| toast.success('Browser notifications enabled!'); | |
| } else { | |
| setBrowserNotificationsEnabled(false); | |
| toast.error('Browser notification permission denied'); | |
| } | |
| } catch (error) { | |
| console.error('Error requesting notification permission:', error); | |
| toast.error('Failed to enable browser notifications'); | |
| } | |
| }; | |
| // Show a browser notification | |
| const showBrowserNotification = (title: string, body: string, icon: string) => { | |
| if (!browserNotificationsSupported || !browserNotificationsEnabled || Notification.permission !== 'granted') { | |
| return; | |
| } | |
| try { | |
| // Create notification options with specific settings for Windows | |
| const options = { | |
| body, | |
| icon, | |
| silent: false, | |
| // Windows-specific options | |
| requireInteraction: true, // Keep notification until user dismisses it | |
| tag: 'work-schedule', // Group notifications with same tag | |
| // Add badge for supported platforms | |
| badge: icon, | |
| // Vibration pattern for mobile devices (doesn't affect Windows) | |
| vibrate: [200, 100, 200] | |
| }; | |
| // Create notification with enhanced options | |
| const notification = new Notification(title, options); | |
| // Close the notification after 30 seconds (increased from 10 to ensure visibility) | |
| setTimeout(() => notification.close(), 30000); | |
| // Focus the window when notification is clicked | |
| notification.onclick = (event) => { | |
| console.log('Notification clicked:', event); | |
| window.focus(); | |
| notification.close(); | |
| }; | |
| // Log when notification is shown or closed | |
| notification.onshow = () => console.log('Notification shown'); | |
| notification.onclose = () => console.log('Notification closed'); | |
| notification.onerror = (err) => console.error('Notification error:', err); | |
| } catch (error) { | |
| console.error('Error showing browser notification:', error); | |
| } | |
| }; | |
| // Update current time every minute | |
| useEffect(() => { | |
| const timer = setInterval(() => { | |
| setCurrentTime(new Date()); | |
| }, 60000); | |
| return () => clearInterval(timer); | |
| }, []); | |
| // Daily schedule data | |
| const scheduleItems: ScheduleItem[] = [ | |
| { | |
| time: "9:30 AM – 10:30 AM", | |
| activity: "Team Collaboration / Planning", | |
| type: "light", | |
| icon: <Users className="h-4 w-4" />, | |
| }, | |
| { | |
| time: "10:30 AM – 11:30 AM", | |
| activity: "Deep Work Session 1", | |
| type: "focused", | |
| icon: <Lock className="h-4 w-4" />, | |
| }, | |
| { | |
| time: "11:30 AM – 12:00 PM", | |
| activity: "Team Discussion / Quick Sync", | |
| type: "collaborative", | |
| icon: <Users className="h-4 w-4" />, | |
| }, | |
| { | |
| time: "12:00 PM – 1:00 PM", | |
| activity: "Deep Work Session 2", | |
| type: "focused", | |
| icon: <Lock className="h-4 w-4" />, | |
| }, | |
| { | |
| time: "1:00 PM – 1:45 PM", | |
| activity: "Lunch Break", | |
| type: "rest", | |
| icon: <Coffee className="h-4 w-4" />, | |
| }, | |
| { | |
| time: "1:45 PM – 2:30 PM", | |
| activity: "Team Collaboration / Emails", | |
| type: "light", | |
| icon: <Mail className="h-4 w-4" />, | |
| }, | |
| { | |
| time: "2:30 PM – 3:30 PM", | |
| activity: "Deep Work Session 3", | |
| type: "focused", | |
| icon: <Lock className="h-4 w-4" />, | |
| }, | |
| { | |
| time: "3:30 PM – 4:00 PM", | |
| activity: "Team Discussion / Admin Tasks", | |
| type: "collaborative", | |
| icon: <PanelRight className="h-4 w-4" />, | |
| }, | |
| { | |
| time: "4:00 PM – 4:15 PM", | |
| activity: "Tea Break", | |
| type: "rest", | |
| icon: <Coffee className="h-4 w-4" />, | |
| }, | |
| { | |
| time: "4:15 PM – 4:30 PM", | |
| activity: "Flex / Light Catch-Up", | |
| type: "light", | |
| icon: <Users className="h-4 w-4" />, | |
| }, | |
| { | |
| time: "4:30 PM – 5:30 PM", | |
| activity: "Deep Work Session 4", | |
| type: "focused", | |
| icon: <Lock className="h-4 w-4" />, | |
| }, | |
| { | |
| time: "5:30 PM – 6:30 PM", | |
| activity: "Team Support / Wrap-Up", | |
| type: "light", | |
| icon: <Users className="h-4 w-4" />, | |
| }, | |
| ]; | |
| // Helper function to show a notification | |
| const showNotification = (item: ScheduleItem, isAutomatic: boolean = true) => { | |
| // If notifications are disabled and this is an automatic notification, don't show it | |
| if (!notificationsEnabled && isAutomatic) { | |
| console.log("Notifications are disabled, not showing notification"); | |
| return; | |
| } | |
| // Play notification sound if notifications are enabled | |
| if (notificationsEnabled) { | |
| playNotificationSound(); | |
| } | |
| const title = isAutomatic ? `Time for ${item.activity}` : `Current Activity: ${item.activity}`; | |
| const description = `${item.time} - ${ | |
| item.type === "focused" ? "Focus on deep work" : | |
| item.type === "collaborative" ? "Connect with your team" : | |
| item.type === "rest" ? "Take a break to recharge" : | |
| "Handle lighter tasks" | |
| }`; | |
| // Show toast notification | |
| toast.info(title, { description }); | |
| // Show browser notification if enabled | |
| if (browserNotificationsEnabled && browserNotificationsPermission === 'granted') { | |
| // Select the appropriate icon based on activity type | |
| let iconPath = '/notification-icon.png'; // Default icon | |
| if (item.type === 'focused') iconPath = '/focused-icon.png'; | |
| else if (item.type === 'collaborative') iconPath = '/collaborative-icon.png'; | |
| else if (item.type === 'rest') iconPath = '/break-icon.png'; | |
| else if (item.type === 'light') iconPath = '/light-icon.png'; | |
| showBrowserNotification(title, description, iconPath); | |
| } | |
| // For prominent notification, show custom modal | |
| setCurrentNotification({ | |
| title, | |
| description, | |
| type: item.type | |
| }); | |
| setShowNotificationModal(true); | |
| // Add a class to the body to prevent scrolling when modal is open | |
| document.body.classList.add('overflow-hidden'); | |
| }; | |
| // Determine current activity based on time and show notifications | |
| useEffect(() => { | |
| const now = currentTime; | |
| const hour = now.getHours(); | |
| const minute = now.getMinutes(); | |
| const currentTimeInMinutes = hour * 60 + minute; | |
| // Find current activity | |
| const index = scheduleItems.findIndex((item) => { | |
| const [startTime, endTime] = item.time.split(" – "); | |
| const [startHour, startMinutePart] = startTime.split(":"); | |
| const startMinute = parseInt(startMinutePart); | |
| const startHourNum = parseInt(startHour) + (startTime.includes("PM") && parseInt(startHour) !== 12 ? 12 : 0); | |
| const startTimeInMinutes = startHourNum * 60 + startMinute; | |
| const [endHour, endMinutePart] = endTime.split(":"); | |
| const endMinute = parseInt(endMinutePart); | |
| const endHourNum = parseInt(endHour) + (endTime.includes("PM") && parseInt(endHour) !== 12 ? 12 : 0); | |
| const endTimeInMinutes = endHourNum * 60 + endMinute; | |
| return currentTimeInMinutes >= startTimeInMinutes && currentTimeInMinutes < endTimeInMinutes; | |
| }); | |
| // Show notification if the activity has changed | |
| if (index !== -1 && index !== lastNotifiedIndex) { | |
| const item = scheduleItems[index]; | |
| // Only show notification if notifications are enabled | |
| if (notificationsEnabled) { | |
| console.log('Notifications enabled, showing notification for:', item.activity); | |
| showNotification(item); | |
| } else { | |
| console.log('Notifications disabled, not showing notification for:', item.activity); | |
| } | |
| setLastNotifiedIndex(index); | |
| } | |
| setCurrentActivityIndex(index); | |
| }, [currentTime, lastNotifiedIndex, scheduleItems, notificationsEnabled]); | |
| // Background color and icon helpers | |
| const getModalStyles = (type: "focused" | "collaborative" | "rest" | "light") => { | |
| switch (type) { | |
| case "focused": | |
| return "bg-purple-50 border-purple-200 text-purple-800"; | |
| case "collaborative": | |
| return "bg-blue-50 border-blue-200 text-blue-800"; | |
| case "rest": | |
| return "bg-red-50 border-red-200 text-red-800"; | |
| case "light": | |
| return "bg-green-50 border-green-200 text-green-800"; | |
| default: | |
| return ""; | |
| } | |
| }; | |
| const getModalIcon = (type: "focused" | "collaborative" | "rest" | "light") => { | |
| switch (type) { | |
| case "focused": | |
| return <Lock className="h-12 w-12 text-purple-600" />; | |
| case "collaborative": | |
| return <Users className="h-12 w-12 text-blue-600" />; | |
| case "rest": | |
| return <Coffee className="h-12 w-12 text-red-600" />; | |
| case "light": | |
| return <Mail className="h-12 w-12 text-green-600" />; | |
| default: | |
| return null; | |
| } | |
| }; | |
| // Get badge style based on activity type | |
| const getBadgeVariant = (type: string, isActive: boolean) => { | |
| if (isActive) { | |
| return "default"; | |
| } | |
| switch (type) { | |
| case "focused": | |
| return "outline"; | |
| case "collaborative": | |
| return "secondary"; | |
| case "rest": | |
| return "destructive"; | |
| case "light": | |
| return "outline"; | |
| default: | |
| return "outline"; | |
| } | |
| }; | |
| // Close the modal | |
| const closeModal = () => { | |
| setShowNotificationModal(false); | |
| document.body.classList.remove('overflow-hidden'); | |
| }; | |
| // Close notification modal when notifications are disabled | |
| useEffect(() => { | |
| if (!notificationsEnabled && showNotificationModal) { | |
| console.log('Notifications disabled, closing notification modal'); | |
| closeModal(); | |
| } | |
| }, [notificationsEnabled]); | |
| // Handle escape key press to close modal | |
| useEffect(() => { | |
| const handleEscapeKey = (event: KeyboardEvent) => { | |
| if (event.key === 'Escape' && showNotificationModal) { | |
| closeModal(); | |
| } | |
| }; | |
| document.addEventListener('keydown', handleEscapeKey); | |
| return () => { | |
| document.removeEventListener('keydown', handleEscapeKey); | |
| }; | |
| }, [showNotificationModal]); | |
| // Test browser notifications with Windows notification features | |
| const testBrowserNotification = () => { | |
| console.log("Testing browser notification"); | |
| console.log("Browser notifications supported:", browserNotificationsSupported); | |
| console.log("Browser notifications enabled:", browserNotificationsEnabled); | |
| console.log("Browser notifications permission:", browserNotificationsPermission); | |
| if (!browserNotificationsSupported) { | |
| toast.error("Browser notifications are not supported in this browser"); | |
| return; | |
| } | |
| if (browserNotificationsPermission !== 'granted') { | |
| toast.warning("Browser notification permission not granted", { | |
| description: "Click the button below to request permission", | |
| action: { | |
| label: "Request Permission", | |
| onClick: () => requestBrowserNotificationPermission() | |
| } | |
| }); | |
| return; | |
| } | |
| if (!browserNotificationsEnabled) { | |
| toast.warning("Browser notifications are disabled", { | |
| description: "Enable them in the notification settings", | |
| action: { | |
| label: "Enable", | |
| onClick: () => setBrowserNotificationsEnabled(true) | |
| } | |
| }); | |
| return; | |
| } | |
| // Show a test notification | |
| try { | |
| // Create notification options with specific settings for Windows | |
| const options = { | |
| body: "This is a test Windows notification. If you can see this outside your browser, notifications are working correctly!", | |
| icon: "/notification-icon.png", | |
| silent: false, | |
| // Windows-specific options | |
| requireInteraction: true, // Keep notification until user dismisses it | |
| tag: 'work-schedule-test', // Group notifications with same tag | |
| // Add badge for supported platforms | |
| badge: "/notification-icon.png", | |
| // Vibration pattern for mobile devices (doesn't affect Windows) | |
| vibrate: [200, 100, 200] | |
| }; | |
| // Create notification with enhanced options | |
| const notification = new Notification("Windows Test Notification", options); | |
| // Handle notification events | |
| notification.onclick = () => { | |
| window.focus(); | |
| notification.close(); | |
| }; | |
| // Log notification events | |
| notification.onshow = () => console.log('Test notification shown'); | |
| notification.onclose = () => console.log('Test notification closed'); | |
| notification.onerror = (err) => console.error('Test notification error:', err); | |
| toast.success("Test Windows notification sent!", { | |
| description: "Check your Windows notification center if you don't see it in the browser." | |
| }); | |
| } catch (error) { | |
| console.error('Error showing test notification:', error); | |
| toast.error("Failed to show test notification", { | |
| description: error.toString() | |
| }); | |
| } | |
| }; | |
| const testNotificationSound = () => { | |
| console.log("Testing notification sound"); | |
| // Initialize audio system if not already done | |
| if (!audioInitialized) { | |
| console.log("Audio not initialized, initializing now"); | |
| initializeAudio(); | |
| } | |
| // Try different methods to play the sound for testing | |
| // Method 1: DOM audio element | |
| const audioElement = document.getElementById('notification-sound-test') as HTMLAudioElement; | |
| if (audioElement) { | |
| console.log("Testing via DOM audio element"); | |
| audioElement.currentTime = 0; | |
| audioElement.volume = 0.5; | |
| const playPromise = audioElement.play(); | |
| if (playPromise !== undefined) { | |
| playPromise | |
| .then(() => { | |
| console.log("DOM audio played successfully"); | |
| setAudioInitialized(true); | |
| toast.success("Sound played successfully!"); | |
| }) | |
| .catch(err => { | |
| console.error("Could not play DOM audio", err); | |
| // Fall back to our ref method | |
| console.log("Falling back to ref-based audio"); | |
| // Method 2: Ref-based audio | |
| if (notificationAudioRef.current) { | |
| notificationAudioRef.current.currentTime = 0; | |
| notificationAudioRef.current.volume = 0.5; | |
| notificationAudioRef.current.play() | |
| .then(() => { | |
| console.log("Ref audio played successfully"); | |
| setAudioInitialized(true); | |
| toast.success("Sound played successfully (fallback method)!"); | |
| }) | |
| .catch(refErr => { | |
| console.error("Ref audio also failed:", refErr); | |
| // Method 3: Try Web Audio API | |
| try { | |
| console.log("Trying Web Audio API beep"); | |
| const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); | |
| const oscillator = audioContext.createOscillator(); | |
| const gainNode = audioContext.createGain(); | |
| oscillator.type = 'sine'; | |
| oscillator.frequency.value = 800; // frequency in hertz | |
| gainNode.gain.value = 0.5; | |
| oscillator.connect(gainNode); | |
| gainNode.connect(audioContext.destination); | |
| oscillator.start(); | |
| oscillator.stop(audioContext.currentTime + 0.3); // beep for 300ms | |
| toast.success("Using synthetic beep instead", { | |
| description: "Your browser is blocking normal audio playback" | |
| }); | |
| // Clean up context after use | |
| setTimeout(() => { | |
| audioContext.close().catch(console.error); | |
| }, 500); | |
| } catch (apiErr) { | |
| console.error("Web Audio API also failed:", apiErr); | |
| toast.error("Could not play any sound", { | |
| description: "Browser is blocking audio playback. Try clicking this toast to enable audio.", | |
| action: { | |
| label: "Try Again", | |
| onClick: initializeAudio | |
| } | |
| }); | |
| } | |
| }); | |
| } else { | |
| toast.error("Audio system not initialized properly"); | |
| } | |
| }); | |
| } | |
| } else { | |
| // Fall back directly to playNotificationSound | |
| playNotificationSound(); | |
| toast.info("Testing sound playback..."); | |
| } | |
| }; | |
| return ( | |
| <> | |
| <Card className="hover-scale"> | |
| <CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0"> | |
| <div> | |
| <CardTitle className="text-lg flex items-center gap-2"> | |
| <Clock className="h-5 w-5 text-muted-foreground" /> | |
| Daily Work Schedule | |
| </CardTitle> | |
| <CardDescription> | |
| Office Hours: 9:30 AM – 6:30 PM | |
| </CardDescription> | |
| </div> | |
| <div className="flex items-center gap-4"> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-sm text-muted-foreground font-medium"> | |
| {notificationsEnabled ? ( | |
| <span className="flex items-center gap-1"> | |
| <Bell className="h-3.5 w-3.5" /> | |
| On | |
| </span> | |
| ) : ( | |
| <span className="flex items-center gap-1"> | |
| <BellOff className="h-3.5 w-3.5" /> | |
| Off | |
| </span> | |
| )} | |
| </span> | |
| <Switch | |
| checked={notificationsEnabled} | |
| onCheckedChange={setNotificationsEnabled} | |
| aria-label="Toggle notifications" | |
| /> | |
| </div> | |
| <div className="text-sm font-medium"> | |
| Current Time: {currentTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} | |
| </div> | |
| <button | |
| onClick={() => { | |
| if (currentActivityIndex >= 0) { | |
| const item = scheduleItems[currentActivityIndex]; | |
| console.log("Manual notification clicked:", item); | |
| // Use the common notification logic, passing false to indicate it's not automatic | |
| showNotification(item, false); | |
| } else { | |
| console.log("No current activity to notify about"); | |
| toast.warning("No Current Activity", { | |
| description: "There is no scheduled activity at this time.", | |
| }); | |
| } | |
| }} | |
| className="p-1 rounded-full hover:bg-accent" | |
| aria-label="Show current activity notification" | |
| > | |
| <Bell className="h-4 w-4 text-muted-foreground" /> | |
| </button> | |
| <button | |
| onClick={testNotificationSound} | |
| className="p-1 rounded-full hover:bg-accent" | |
| title="Test notification sound" | |
| aria-label="Test notification sound" | |
| > | |
| <BellRing className="h-4 w-4 text-muted-foreground" /> | |
| </button> | |
| </div> | |
| </CardHeader> | |
| {/* Audio element for testing purposes (can be hidden with CSS) */} | |
| <audio | |
| id="notification-sound-test" | |
| src="/notification.mp3" | |
| preload="auto" | |
| style={{ display: 'none' }} | |
| /> | |
| <CardContent> | |
| <div className="space-y-3"> | |
| {scheduleItems.map((item, index) => { | |
| const isCurrentActivity = index === currentActivityIndex; | |
| return ( | |
| <div | |
| key={index} | |
| className={`flex items-center justify-between p-2 rounded-md ${ | |
| isCurrentActivity | |
| ? 'bg-accent border-l-4 border-primary animate-pulse' | |
| : '' | |
| }`} | |
| > | |
| <div className="flex items-center gap-3"> | |
| <Badge | |
| variant={getBadgeVariant(item.type, isCurrentActivity)} | |
| className={`w-8 h-8 rounded-full flex items-center justify-center ${ | |
| item.type === "focused" ? "bg-purple-100 text-purple-600 hover:bg-purple-200" : | |
| item.type === "rest" ? "bg-red-100 text-red-600 hover:bg-red-200" : | |
| item.type === "collaborative" ? "bg-blue-100 text-blue-600 hover:bg-blue-200" : | |
| "bg-green-100 text-green-600 hover:bg-green-200" | |
| }`} | |
| > | |
| {item.icon} | |
| </Badge> | |
| <div> | |
| <p className="text-sm font-medium">{item.activity}</p> | |
| <p className="text-xs text-muted-foreground">{item.time}</p> | |
| </div> | |
| </div> | |
| <div> | |
| <Badge | |
| variant={item.type === "focused" ? "default" : "outline"} | |
| className={` | |
| ${item.type === "focused" ? "bg-purple-100 text-purple-600 hover:bg-purple-200 border-purple-200" : | |
| item.type === "rest" ? "bg-red-100 text-red-600 hover:bg-red-200 border-red-200" : | |
| item.type === "collaborative" ? "bg-blue-100 text-blue-600 hover:bg-blue-200 border-blue-200" : | |
| "bg-green-100 text-green-600 hover:bg-green-200 border-green-200"} | |
| ${isCurrentActivity ? "ring-2 ring-offset-1 ring-primary" : ""} | |
| `} | |
| > | |
| {item.type === "focused" ? "Deep Work" : | |
| item.type === "collaborative" ? "Collaborative" : | |
| item.type === "rest" ? "Break" : "Light Work"} | |
| </Badge> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| {/* Custom Large Notification Modal */} | |
| {showNotificationModal && currentNotification && ( | |
| <div className="fixed inset-0 z-50 flex items-center justify-center"> | |
| {/* Backdrop */} | |
| <div | |
| className="absolute inset-0 bg-black bg-opacity-50 backdrop-blur-sm" | |
| onClick={closeModal} | |
| /> | |
| {/* Modal Content */} | |
| <div | |
| className={`relative mx-4 w-full max-w-md rounded-lg border shadow-lg transform transition-all ${getModalStyles(currentNotification.type)}`} | |
| style={{ animation: 'scaleIn 0.3s ease-out' }} | |
| > | |
| {/* Close button */} | |
| <button | |
| onClick={closeModal} | |
| className="absolute right-4 top-4 rounded-full p-1 text-gray-500 hover:bg-gray-100" | |
| > | |
| <X className="h-5 w-5" /> | |
| </button> | |
| <div className="p-6"> | |
| {/* Header */} | |
| <div className="flex items-start gap-4"> | |
| <div className="p-3 rounded-full bg-white shadow-md"> | |
| {getModalIcon(currentNotification.type)} | |
| </div> | |
| <div className="flex-1 pt-2"> | |
| <h2 className="text-xl font-bold">{currentNotification.title}</h2> | |
| <p className="text-base mt-1 opacity-90"> | |
| {currentNotification.description} | |
| </p> | |
| </div> | |
| </div> | |
| {/* Additional information based on activity type */} | |
| <div className="mt-6 p-4 rounded-md bg-white bg-opacity-50"> | |
| {currentNotification.type === "focused" ? ( | |
| <p className="text-sm">🧠 Time to focus! Close distractions, silence notifications, and dedicate this time to deep, meaningful work.</p> | |
| ) : currentNotification.type === "collaborative" ? ( | |
| <p className="text-sm">👥 Connect with your team! This is a good time for meetings, discussions, and collaborative problem-solving.</p> | |
| ) : currentNotification.type === "rest" ? ( | |
| <p className="text-sm">🔋 Take a break! Step away from your desk, stretch, or grab a refreshment to recharge your energy.</p> | |
| ) : ( | |
| <p className="text-sm">📋 Handle lighter tasks! This is a good time for emails, planning, and administrative tasks.</p> | |
| )} | |
| </div> | |
| {/* Footer */} | |
| <div className="mt-6 space-y-3"> | |
| <div className="flex justify-between items-center"> | |
| <div className="flex items-center gap-2"> | |
| <Switch | |
| id="notification-toggle" | |
| checked={notificationsEnabled} | |
| onCheckedChange={setNotificationsEnabled} | |
| aria-label="Toggle notifications" | |
| /> | |
| <label | |
| htmlFor="notification-toggle" | |
| className="text-sm cursor-pointer" | |
| > | |
| {notificationsEnabled ? "Notifications On" : "Notifications Off"} | |
| </label> | |
| </div> | |
| {browserNotificationsSupported && ( | |
| <div className="flex items-center gap-2"> | |
| <Switch | |
| id="browser-notification-toggle" | |
| checked={browserNotificationsEnabled} | |
| onCheckedChange={(checked) => { | |
| if (checked && browserNotificationsPermission !== 'granted') { | |
| requestBrowserNotificationPermission(); | |
| } else { | |
| setBrowserNotificationsEnabled(checked); | |
| } | |
| }} | |
| disabled={browserNotificationsPermission === 'denied'} | |
| aria-label="Toggle browser notifications" | |
| /> | |
| <label | |
| htmlFor="browser-notification-toggle" | |
| className="text-sm cursor-pointer" | |
| > | |
| Browser Notifications | |
| </label> | |
| </div> | |
| )} | |
| </div> | |
| <div className="flex justify-end"> | |
| <Button | |
| onClick={closeModal} | |
| className={`font-semibold ${ | |
| currentNotification.type === "focused" ? "bg-purple-600 hover:bg-purple-700" : | |
| currentNotification.type === "collaborative" ? "bg-blue-600 hover:bg-blue-700" : | |
| currentNotification.type === "rest" ? "bg-red-600 hover:bg-red-700" : | |
| "bg-green-600 hover:bg-green-700" | |
| } text-white`} | |
| > | |
| Acknowledge | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Add the animation styles */} | |
| <style dangerouslySetInnerHTML={{ | |
| __html: ` | |
| @keyframes scaleIn { | |
| from { opacity: 0; transform: scale(0.9); } | |
| to { opacity: 1; transform: scale(1); } | |
| } | |
| .overflow-hidden { | |
| overflow: hidden; | |
| } | |
| `}} /> | |
| </> | |
| ); | |
| } |