From 4e8feba5fecf8aae7b331100c7169e3c52bef6ec Mon Sep 17 00:00:00 2001 From: AG Date: Fri, 19 Dec 2025 13:00:47 +0200 Subject: [PATCH] Timer Signal on Mobile with Notification --- src/components/ui/RestTimerFAB.tsx | 48 ++++++++++- src/hooks/timer.worker.ts | 81 +++++++++++++++++++ src/hooks/useRestTimer.ts | 123 ++++++++++++++++++++++++++--- src/utils/notifications.ts | 80 +++++++++++++++++++ 4 files changed, 318 insertions(+), 14 deletions(-) create mode 100644 src/hooks/timer.worker.ts create mode 100644 src/utils/notifications.ts diff --git a/src/components/ui/RestTimerFAB.tsx b/src/components/ui/RestTimerFAB.tsx index fe0b9a4..3a389ee 100644 --- a/src/components/ui/RestTimerFAB.tsx +++ b/src/components/ui/RestTimerFAB.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from 'react'; -import { Timer, Play, Pause, RotateCcw, Edit2, Plus, Minus, X, Check } from 'lucide-react'; +import { Timer, Play, Pause, RotateCcw, Edit2, Plus, Minus, X, Check, Bell, BellOff } from 'lucide-react'; import { useRestTimer } from '../../hooks/useRestTimer'; +import { requestNotificationPermission } from '../../utils/notifications'; interface RestTimerFABProps { timer: ReturnType; @@ -21,6 +22,10 @@ const RestTimerFAB: React.FC = ({ timer, onDurationChange }) const [isEditing, setIsEditing] = useState(false); const [editValue, setEditValue] = useState(120); const [inputValue, setInputValue] = useState(formatSeconds(120)); + const [notificationPermission, setNotificationPermission] = useState( + 'Notification' in window ? Notification.permission : 'default' + ); + const [isSecure, setIsSecure] = useState(true); // Auto-expand when running if not already expanded? No, requirement says "when time is running, show digits of the countdown on the enlarged timer FAB even if the menu is collapsed". // So the FAB itself grows. @@ -38,6 +43,20 @@ const RestTimerFAB: React.FC = ({ timer, onDurationChange }) setInputValue(formatSeconds(editValue)); }, [editValue]); + // Check permission on mount and focus + useEffect(() => { + const checkState = () => { + if ('Notification' in window) { + setNotificationPermission(Notification.permission); + } + setIsSecure(window.isSecureContext); + }; + + checkState(); + window.addEventListener('focus', checkState); + return () => window.removeEventListener('focus', checkState); + }, []); + const handleToggle = () => { if (isEditing) return; // Don't toggle if editing setIsExpanded(!isExpanded); @@ -54,6 +73,18 @@ const RestTimerFAB: React.FC = ({ timer, onDurationChange }) reset(); }; + const handleRequestPermission = async (e: React.MouseEvent) => { + e.stopPropagation(); + + if (!isSecure) { + alert("Notifications require a secure context (HTTPS) or localhost."); + return; + } + + const result = await requestNotificationPermission(); + if (result) setNotificationPermission(result); + }; + const handleEdit = (e: React.MouseEvent) => { e.stopPropagation(); const initialVal = timeLeft > 0 ? timeLeft : 120; @@ -189,6 +220,21 @@ const RestTimerFAB: React.FC = ({ timer, onDurationChange }) ) : (
+ {/* Notification Permission Button (Only if not granted) */} + {notificationPermission !== 'granted' && 'Notification' in window && ( + + )} + {/* Mini FABs */}