feat: implement complete web push notification system

- Add self-hosted push notifications using VAPID (no external services)
- Create PushSubscription model in database
- Implement server-side push logic with web-push library
- Add notification helpers for list events (items, invitations, collaborators)
- Create NotificationSettings UI component for profile page
- Add usePushNotifications React hook for subscription management
- Create API routes for subscription management and VAPID key
- Update service worker with push event handlers
- Integrate notifications into item actions (add, remove, complete)
- Integrate notifications into sharing actions (invite, accept, remove)
- Add VAPID key generation script
- Add comprehensive documentation and setup guides

Notifications are sent when:
- Items are added/removed/completed in shared lists
- Users are invited to lists
- Invitations are accepted
- Collaborators are removed from lists

Works on iOS 16.4+ (when installed to home screen), Android, and desktop browsers.
This commit is contained in:
2025-12-07 01:22:16 +00:00
parent 625ca40e48
commit 7cda58440f
21 changed files with 2144 additions and 8 deletions

View File

@@ -12,6 +12,11 @@ import {
validateTags,
sanitizeString,
} from './validation'
import {
notifyItemAdded,
notifyItemRemoved,
notifyItemCompleted,
} from './notification-helpers'
// Retry utility for database operations
async function retryOperation<T>(
@@ -370,6 +375,14 @@ export async function createItem(
})
)
// Send notification to collaborators
try {
await notifyItemAdded(listId, item.title, userId)
} catch (error) {
console.error('Failed to send notification:', error)
// Don't fail the action if notification fails
}
revalidatePath(`/list/${listId}`)
return item
} catch (error) {
@@ -437,6 +450,15 @@ export async function toggleItem(itemId: string) {
})
)
// Send notification when item is completed (not uncompleted)
if (updated.completed) {
try {
await notifyItemCompleted(item.listId, updated.title, userId)
} catch (error) {
console.error('Failed to send notification:', error)
}
}
revalidatePath(`/list/${item.listId}`)
return updated
} catch (error) {
@@ -609,6 +631,13 @@ export async function deleteItem(itemId: string) {
})
)
// Send notification to collaborators
try {
await notifyItemRemoved(item.listId, item.title, userId)
} catch (error) {
console.error('Failed to send notification:', error)
}
revalidatePath(`/list/${item.listId}`)
return { success: true }
} catch (error) {