managarten/packages/shared-stores/src/notifications.ts
Till JS 4fa096147c feat(shared-stores,shared-ui): add shared reminder system
Add notificationService (Browser Notification API wrapper),
createReminderScheduler (30s poller with source pattern for checking
due reminders), and ReminderPicker UI component.

Todo module gets todoReminderSource (checks task dueDate - minutesBefore)
and ReminderSelector now delegates to shared ReminderPicker.

Scheduler supports multiple sources (todo, calendar, planta, etc.),
tag-based dedup, graceful error handling, and runtime source addition.
22 new tests (8 notification + 8 scheduler + 6 ReminderPicker).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:54:15 +02:00

68 lines
1.8 KiB
TypeScript

/**
* Browser Notification Service
*
* Centralized wrapper for the Browser Notification API.
* Used by the reminder scheduler to fire local notifications.
*
* @example
* ```typescript
* import { notificationService } from '@manacore/shared-stores';
*
* if (await notificationService.requestPermission()) {
* notificationService.send('Task fällig', { body: 'Einkaufen gehen' });
* }
* ```
*/
export interface NotificationOptions {
/** Notification body text */
body?: string;
/** Icon URL */
icon?: string;
/** Tag for deduplication (same tag replaces previous notification) */
tag?: string;
/** Called when user clicks the notification */
onClick?: () => void;
}
export const notificationService = {
/** Check if browser supports Notification API */
isSupported(): boolean {
return typeof window !== 'undefined' && 'Notification' in window;
},
/** Check if permission is already granted */
hasPermission(): boolean {
if (!this.isSupported()) return false;
return Notification.permission === 'granted';
},
/** Request notification permission. Returns true if granted. */
async requestPermission(): Promise<boolean> {
if (!this.isSupported()) return false;
if (Notification.permission === 'granted') return true;
if (Notification.permission === 'denied') return false;
const result = await Notification.requestPermission();
return result === 'granted';
},
/** Send a browser notification. No-op if permission not granted. */
send(title: string, options?: NotificationOptions): void {
if (!this.hasPermission()) return;
const notification = new Notification(title, {
body: options?.body,
icon: options?.icon ?? '/favicon.png',
tag: options?.tag,
});
if (options?.onClick) {
notification.onclick = () => {
window.focus();
options.onClick!();
notification.close();
};
}
},
};