Enable notifications to receive alarm alerts Enable `; }; // ============================================ // TIME PARSING UTILITY // ============================================ const TimeParser = { parse(input) { if (!input) return null; const str = input.trim().toLowerCase(); // Try "HH:MM" or "H:MM" (24h or 12h without am/pm) let match = str.match(/^(\d{1,2}):(\d{2})$/); if (match) { let h = parseInt(match[1], 10); let m = parseInt(match[2], 10); if (h >= 0 && h <= 23 && m >= 0 && m <= 59) { return this.format(h, m); } } // Try "H:MM am/pm" or "HH:MM am/pm" match = str.match(/^(\d{1,2}):(\d{2})\s*(am|pm|a|p)$/); if (match) { let h = parseInt(match[1], 10); let m = parseInt(match[2], 10); const period = match[3]; if (h >= 1 && h <= 12 && m >= 0 && m <= 59) { if (period.startsWith('p') && h !== 12) h += 12; if (period.startsWith('a') && h === 12) h = 0; return this.format(h, m); } } // Try "HMM" or "HHMM" (e.g., "930" or "1430") match = str.match(/^(\d{3,4})$/); if (match) { const num = match[1]; let h, m; if (num.length === 3) { h = parseInt(num[0], 10); m = parseInt(num.slice(1), 10); } else { h = parseInt(num.slice(0, 2), 10); m = parseInt(num.slice(2), 10); } if (h >= 0 && h <= 23 && m >= 0 && m <= 59) { return this.format(h, m); } } // Try "HMMam/pm" or "HHMMam/pm" (e.g., "930am" or "1130pm") match = str.match(/^(\d{3,4})\s*(am|pm|a|p)$/); if (match) { const num = match[1]; const period = match[2]; let h, m; if (num.length === 3) { h = parseInt(num[0], 10); m = parseInt(num.slice(1), 10); } else { h = parseInt(num.slice(0, 2), 10); m = parseInt(num.slice(2), 10); } if (h >= 1 && h <= 12 && m >= 0 && m <= 59) { if (period.startsWith('p') && h !== 12) h += 12; if (period.startsWith('a') && h === 12) h = 0; return this.format(h, m); } } // Try just hour "H am/pm" or "HH am/pm" match = str.match(/^(\d{1,2})\s*(am|pm|a|p)$/); if (match) { let h = parseInt(match[1], 10); const period = match[2]; if (h >= 1 && h <= 12) { if (period.startsWith('p') && h !== 12) h += 12; if (period.startsWith('a') && h === 12) h = 0; return this.format(h, 0); } } return null; }, format(h, m) { return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; }, toDisplay(time24) { if (!time24) return ''; const [h, m] = time24.split(':').map(Number); const period = h >= 12 ? 'PM' : 'AM'; const h12 = h % 12 || 12; return `${h12}:${String(m).padStart(2, '0')} ${period}`; } }; const AlarmForm = ({ onAdd }) => { const [label, setLabel] = useState(''); const [hour, setHour] = useState(8); const [minute, setMinute] = useState(0); const [period, setPeriod] = useState('AM'); const addInMinutes = (minutes) => { const now = new Date(); now.setMinutes(now.getMinutes() + minutes); const time = TimeParser.format(now.getHours(), now.getMinutes()); onAdd(time, label || `In ${minutes} min`); setLabel(''); }; const addAtTime = () => { let h = hour; if (period === 'PM' && h !== 12) h += 12; if (period === 'AM' && h === 12) h = 0; const time = TimeParser.format(h, minute); onAdd(time, label); setLabel(''); }; const presets = [ { label: '1 min', minutes: 1 }, { label: '5 min', minutes: 5 }, { label: '10 min', minutes: 10 }, { label: '30 min', minutes: 30 }, { label: '1 hour', minutes: 60 }, ]; const hours = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; const minutes = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]; return html` Quick alarm ${presets.map(p => html` addInMinutes(p.minutes)} > ${p.label} `)} New alarm setHour(parseInt(e.target.value))} > ${hours.map(h => html`${h}`)} : setMinute(parseInt(e.target.value))} > ${minutes.map(m => html`${String(m).padStart(2, '0')}`)} setPeriod('AM')} >AM setPeriod('PM')} >PM setLabel(e.target.value)} onKeyDown=${(e) => { if (e.key === 'Enter') addAtTime(); }} placeholder="Label — Wake up, Meeting…" /> Set alarm `; }; const AlarmItem = ({ alarm, onToggle, onDelete, onDismiss }) => { const display = TimeParser.toDisplay(alarm.time); const [main, ap] = display.split(' '); return html` ${main}${ap} ${alarm.label || 'Alarm'} ${alarm.ringing ? html` onDismiss(alarm.id)}> Dismiss onDelete(alarm.id)} > <${DeleteIcon} /> ` : html` onToggle(alarm.id)} aria-label="Enable alarm ${display}" /> onDelete(alarm.id)} > <${DeleteIcon} /> `} `; }; const EmptyState = () => html` <${BellIcon} /> No alarms yet Add your first one above `; const App = () => { const [alarms, setAlarms] = useState([]); // Only prompt when the API exists and the user hasn't already decided: // with permission 'denied' (or no Notification API at all) the button // could never do anything. const [showNotificationPrompt, setShowNotificationPrompt] = useState( ('Notification' in window) && NotificationService.permission === 'default' ); useEffect(() => { alarmService.init(); const unsubscribe = alarmService.subscribe(setAlarms); return () => { unsubscribe(); alarmService.stopChecking(); }; }, []); const handleAddAlarm = useCallback((time, label) => { alarmService.addAlarm(time, label); toast(`Alarm set for ${TimeParser.toDisplay(time)}`); }, []); const handleToggle = useCallback((id) => { alarmService.toggleAlarm(id); }, []); const handleDelete = useCallback((id) => { alarmService.removeAlarm(id); }, []); const handleDismiss = useCallback((id) => { alarmService.dismissAlarm(id); }, []); const sortedAlarms = [...alarms].sort((a, b) => a.time.localeCompare(b.time)); return html` <${HeroClock} alarms=${alarms} /> ${showNotificationPrompt && html` <${NotificationPrompt} onDone=${() => setShowNotificationPrompt(false)} /> `} <${AlarmForm} onAdd=${handleAddAlarm} /> Your alarms ${sortedAlarms.length === 0 ? html`<${EmptyState} />` : sortedAlarms.map(alarm => html` <${AlarmItem} key=${alarm.id} alarm=${alarm} onToggle=${handleToggle} onDelete=${handleDelete} onDismiss=${handleDismiss} /> `) } `; }; // ============================================ // RENDER // ============================================ render(html`<${App} />`, document.getElementById('app')); // Expose services for debugging/extension window.AlarmApp = { storageService, alarmService, NotificationService, AudioService, StorageBackends };
No alarms yet
Add your first one above `; const App = () => { const [alarms, setAlarms] = useState([]); // Only prompt when the API exists and the user hasn't already decided: // with permission 'denied' (or no Notification API at all) the button // could never do anything. const [showNotificationPrompt, setShowNotificationPrompt] = useState( ('Notification' in window) && NotificationService.permission === 'default' ); useEffect(() => { alarmService.init(); const unsubscribe = alarmService.subscribe(setAlarms); return () => { unsubscribe(); alarmService.stopChecking(); }; }, []); const handleAddAlarm = useCallback((time, label) => { alarmService.addAlarm(time, label); toast(`Alarm set for ${TimeParser.toDisplay(time)}`); }, []); const handleToggle = useCallback((id) => { alarmService.toggleAlarm(id); }, []); const handleDelete = useCallback((id) => { alarmService.removeAlarm(id); }, []); const handleDismiss = useCallback((id) => { alarmService.dismissAlarm(id); }, []); const sortedAlarms = [...alarms].sort((a, b) => a.time.localeCompare(b.time)); return html`