mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 00:22:13 +02:00
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>
This commit is contained in:
parent
b995d52146
commit
4fa096147c
11 changed files with 624 additions and 26 deletions
68
packages/shared-stores/src/notifications.ts
Normal file
68
packages/shared-stores/src/notifications.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* 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();
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue