feat: Add Web Push notifications and multiple UX improvements
continuous-integration/drone/push Build is failing

- Add PWA manifest and service worker for push notifications
- Implement VAPID key generation and push subscription management
- Add push notification API endpoints (/api/push/*)
- Add push_subscriptions table to database schema
- Update notification settings UI with push support and iOS hints
- Fix translation issue showing "{ task } created" - add missing keys
- Fix mobile sidebar visibility for iOS home screen app
- Change default task filter from "all" to "open" (excludes done tasks)
- Add "Open" filter option to show only todo + inProgress tasks
This commit is contained in:
Paul Nothaft
2026-01-19 20:53:13 +01:00
parent 9bfc9e2f96
commit f91978c078
15 changed files with 1158 additions and 126 deletions
+99 -18
View File
@@ -22,32 +22,113 @@ import { DataExportCard } from '@/components/user/DataExportCard';
const NotificationSettings = () => {
const { t } = useTranslation();
const { enabled, toggleEnabled, permission, requestPermission } = useNotifications({ poll: false });
const { toast } = useToast();
const {
enabled,
toggleEnabled,
permission,
requestPermission,
pushSupported,
isStandalone,
sendTestNotification,
isLoading
} = useNotifications({ poll: false });
const handleToggle = (checked: boolean) => {
const handleToggle = async (checked: boolean) => {
if (checked && permission !== 'granted') {
requestPermission();
const success = await requestPermission();
if (success) {
toast({
title: t('notifications.enabledTitle'),
description: t('notifications.enabledBody'),
});
}
} else {
toggleEnabled(checked);
await toggleEnabled(checked);
}
};
const handleTestNotification = async () => {
const success = await sendTestNotification();
if (success) {
toast({
title: t('notifications.testSent', 'Test Sent'),
description: t('notifications.testSentDesc', 'Check for a notification on your device.'),
});
} else {
toast({
title: t('common.error'),
description: t('notifications.testFailed', 'Failed to send test notification.'),
variant: 'destructive',
});
}
};
return (
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<p className="font-medium">{t('notifications.enableBrowser')}</p>
<p className="text-sm text-muted-foreground">
{permission === 'denied' ?
<span className="text-destructive">Permission denied by browser. Please reset site permissions.</span> :
t('notifications.description')
}
</p>
<div className="space-y-4">
{/* Push Support Status */}
{pushSupported && (
<div className="flex items-center gap-2 text-sm">
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
{t('notifications.pushSupported', 'Push Supported')}
</span>
{isStandalone && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-violet-100 text-violet-800 dark:bg-violet-900/30 dark:text-violet-400">
{t('notifications.installedApp', 'Installed App')}
</span>
)}
</div>
)}
{/* iOS PWA Hint */}
{!pushSupported && /iPhone|iPad|iPod/.test(navigator.userAgent) && !isStandalone && (
<div className="p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800">
<p className="text-sm text-amber-800 dark:text-amber-200">
<strong>{t('notifications.iosHintTitle', 'Add to Home Screen')}</strong><br />
{t('notifications.iosHintDesc', 'To receive push notifications on iOS, tap the Share button and select "Add to Home Screen", then open the app from there.')}
</p>
</div>
)}
{/* Main Toggle */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<p className="font-medium">
{pushSupported
? t('notifications.enablePush', 'Push Notifications')
: t('notifications.enableBrowser')}
</p>
<p className="text-sm text-muted-foreground">
{permission === 'denied' ? (
<span className="text-destructive">
{t('notifications.permissionDenied', 'Permission denied by browser. Please reset site permissions.')}
</span>
) : pushSupported ? (
t('notifications.pushDescription', 'Receive notifications even when the app is closed.')
) : (
t('notifications.description')
)}
</p>
</div>
<Switch
checked={enabled}
onCheckedChange={handleToggle}
disabled={permission === 'denied' || isLoading}
/>
</div>
<Switch
checked={enabled}
onCheckedChange={handleToggle}
disabled={permission === 'denied'}
/>
{/* Test Button */}
{enabled && pushSupported && (
<Button
variant="outline"
size="sm"
onClick={handleTestNotification}
disabled={isLoading}
className="w-full sm:w-auto"
>
{t('notifications.sendTest', 'Send Test Notification')}
</Button>
)}
</div>
);
};