fix:push notification UI, settings and API

Introduce NotificationManager, NotificationPrompt, NotificationSettings,
NotificationSettingsSheet and integrate into the profile panel. Add
server GET/POST endpoints for notification preferences. Add
lucide-svelte dependency and update CSP connect-src to allow
fcm.googleapis.com and android.googleapis.com
This commit is contained in:
2025-11-08 14:27:21 +01:00
parent e27b2498b7
commit 0754d62d0e
9 changed files with 887 additions and 41 deletions

View File

@@ -59,6 +59,7 @@
"@node-rs/argon2": "^2.0.2",
"@sveltejs/adapter-vercel": "^5.10.2",
"arctic": "^3.7.0",
"lucide-svelte": "^0.553.0",
"nanoid": "^5.1.6",
"postgres": "^3.4.5",
"sharp": "^0.34.4",

View File

@@ -52,7 +52,7 @@ export const handle: Handle = async ({ event, resolve }) => {
"font-src 'self' fonts.gstatic.com; " +
"img-src 'self' data: blob: *.openstreetmap.org *.tile.openstreetmap.org *.r2.cloudflarestorage.com *.r2.dev; " +
"media-src 'self' *.r2.cloudflarestorage.com *.r2.dev; " +
"connect-src 'self' *.openstreetmap.org; " +
"connect-src 'self' *.openstreetmap.org https://fcm.googleapis.com https://android.googleapis.com; " +
"frame-ancestors 'none'; " +
"base-uri 'self'; " +
"form-action 'self';"

View File

@@ -1,32 +1,33 @@
<script lang="ts">
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import NotificationPrompt from './NotificationPrompt.svelte';
/**
* NotificationManager - Handles push notification subscription
* Automatically requests permission and subscribes authenticated users
* Shows a prompt for users to enable notifications (requires user gesture for iOS)
*/
let permissionStatus = $state<NotificationPermission>('default');
let subscriptionStatus = $state<'idle' | 'subscribing' | 'subscribed' | 'error'>('idle');
let errorMessage = $state<string>('');
let showPrompt = $state<boolean>(false);
let isSupported = $state<boolean>(false);
const PROMPT_DISMISSED_KEY = 'notification-prompt-dismissed';
onMount(() => {
if (!browser) return;
// Check if notifications are supported
if (!('Notification' in window)) {
console.log('This browser does not support notifications');
// Check if notifications and service workers are supported
isSupported = 'Notification' in window && 'serviceWorker' in navigator;
if (!isSupported) {
console.log('Notifications or service workers not supported in this browser');
return;
}
// Check if service workers are supported
if (!('serviceWorker' in navigator)) {
console.log('This browser does not support service workers');
return;
}
// Initialize notification subscription
// Initialize without requesting permission
initializeNotifications();
});
@@ -37,60 +38,93 @@
// Get current permission status
permissionStatus = Notification.permission;
console.log('[NotificationManager] Permission status:', permissionStatus);
// If already denied, don't do anything
if (permissionStatus === 'denied') {
console.log('Notification permission denied by user');
return;
// If already granted, subscribe automatically
if (permissionStatus === 'granted') {
console.log('[NotificationManager] Permission already granted');
await subscribeToNotifications();
}
// Get existing service worker registration (SvelteKit registers it automatically)
// If permission is default and not dismissed, show prompt
else if (permissionStatus === 'default') {
const dismissed = localStorage.getItem(PROMPT_DISMISSED_KEY);
if (!dismissed) {
showPrompt = true;
}
}
// If denied, do nothing
else {
console.log('[NotificationManager] Permission denied by user');
}
} catch (error) {
console.error('[NotificationManager] Error initializing notifications:', error);
subscriptionStatus = 'error';
errorMessage = error instanceof Error ? error.message : 'Unknown error';
}
}
async function handleEnableNotifications() {
try {
console.log('[NotificationManager] User clicked enable notifications');
showPrompt = false;
// Request permission (this is triggered by user gesture, so iOS will allow it)
permissionStatus = await Notification.requestPermission();
console.log('[NotificationManager] Permission response:', permissionStatus);
if (permissionStatus === 'granted') {
await subscribeToNotifications();
} else {
console.log('[NotificationManager] Permission not granted');
subscriptionStatus = 'error';
errorMessage = 'Notification permission was not granted';
}
} catch (error) {
console.error('[NotificationManager] Error enabling notifications:', error);
subscriptionStatus = 'error';
errorMessage = error instanceof Error ? error.message : 'Unknown error';
}
}
function handleDismissPrompt() {
console.log('[NotificationManager] User dismissed notification prompt');
showPrompt = false;
localStorage.setItem(PROMPT_DISMISSED_KEY, 'true');
}
async function subscribeToNotifications() {
try {
console.log('[NotificationManager] subscribeToNotifications called');
subscriptionStatus = 'subscribing';
// Get or register service worker
let registration = await navigator.serviceWorker.getRegistration();
// If no registration exists, register it
if (!registration) {
console.log('[NotificationManager] No SW found, registering...');
registration = await navigator.serviceWorker.register('/service-worker.js', {
type: 'module'
});
}
// Wait for service worker to be ready
await navigator.serviceWorker.ready;
console.log('[NotificationManager] Service worker ready');
// If permission is default, request it
if (permissionStatus === 'default') {
console.log('[NotificationManager] Requesting permission...');
permissionStatus = await Notification.requestPermission();
console.log('[NotificationManager] Permission response:', permissionStatus);
}
// If permission granted, subscribe to push notifications
if (permissionStatus === 'granted') {
console.log('[NotificationManager] Permission granted, subscribing...');
await subscribeToPushNotifications(registration);
} else {
console.log('[NotificationManager] Permission not granted, status:', permissionStatus);
}
} catch (error) {
console.error('Error initializing notifications:', error);
subscriptionStatus = 'error';
errorMessage = error instanceof Error ? error.message : 'Unknown error';
}
}
async function subscribeToPushNotifications(registration: ServiceWorkerRegistration) {
try {
console.log('[NotificationManager] subscribeToPushNotifications called');
subscriptionStatus = 'subscribing';
// Get VAPID public key from server
console.log('[NotificationManager] Fetching VAPID key...');
const response = await fetch('/api/notifications/subscribe');
if (!response.ok) {
throw new Error('Failed to get VAPID public key');
}
const { publicKey } = await response.json();
console.log('[NotificationManager] Got VAPID key:', publicKey);
// Check if already subscribed
console.log('[NotificationManager] Checking existing subscription...');
let subscription = await registration.pushManager.getSubscription();
console.log('[NotificationManager] Existing subscription:', subscription);
// If not subscribed, create new subscription
if (!subscription) {
console.log('[NotificationManager] Creating new subscription...');
@@ -100,6 +134,7 @@
});
console.log('[NotificationManager] Subscription created:', subscription);
}
// Send subscription to server
console.log('[NotificationManager] Sending subscription to server...');
const saveResponse = await fetch('/api/notifications/subscribe', {
@@ -117,12 +152,15 @@
}
})
});
console.log('[NotificationManager] Save response status:', saveResponse.status);
if (!saveResponse.ok) {
const errorText = await saveResponse.text();
console.error('[NotificationManager] Save failed:', errorText);
throw new Error('Failed to save subscription to server');
}
subscriptionStatus = 'subscribed';
console.log('[NotificationManager] Successfully subscribed to push notifications!');
} catch (error) {
@@ -162,3 +200,7 @@
return window.btoa(binary);
}
</script>
{#if showPrompt}
<NotificationPrompt onEnable={handleEnableNotifications} onDismiss={handleDismissPrompt} />
{/if}

View File

@@ -0,0 +1,173 @@
<script lang="ts">
import { X, Bell } from 'lucide-svelte';
interface Props {
onEnable: () => void;
onDismiss: () => void;
}
let { onEnable, onDismiss }: Props = $props();
</script>
<div class="notification-prompt">
<div class="notification-prompt-content">
<div class="notification-prompt-icon">
<Bell size={20} />
</div>
<div class="notification-prompt-text">
<h3>Enable Notifications</h3>
<p>Stay updated when friends like or comment on your finds</p>
</div>
<div class="notification-prompt-actions">
<button class="enable-button" onclick={onEnable}>Enable</button>
<button class="dismiss-button" onclick={onDismiss} aria-label="Dismiss notification prompt">
<X size={20} />
</button>
</div>
</div>
</div>
<style>
.notification-prompt {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
z-index: 50;
width: 90%;
max-width: 600px;
animation: slideDown 0.3s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateX(-50%) translateY(-20px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
.notification-prompt-content {
background: white;
border-radius: 12px;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
border: 1px solid #e5e7eb;
padding: 16px 20px;
display: flex;
align-items: center;
gap: 16px;
}
.notification-prompt-icon {
flex-shrink: 0;
width: 40px;
height: 40px;
background: #3b82f6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.notification-prompt-text {
flex: 1;
min-width: 0;
}
.notification-prompt-text h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #111827;
line-height: 1.3;
}
.notification-prompt-text p {
margin: 4px 0 0 0;
font-size: 14px;
color: #6b7280;
line-height: 1.4;
}
.notification-prompt-actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.enable-button {
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
white-space: nowrap;
}
.enable-button:hover {
background: #2563eb;
}
.enable-button:active {
background: #1d4ed8;
}
.dismiss-button {
background: transparent;
border: none;
color: #9ca3af;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}
.dismiss-button:hover {
background: #f3f4f6;
color: #6b7280;
}
@media (max-width: 768px) {
.notification-prompt {
top: 70px;
width: 95%;
}
.notification-prompt-content {
padding: 12px 16px;
gap: 12px;
}
.notification-prompt-icon {
width: 36px;
height: 36px;
}
.notification-prompt-text h3 {
font-size: 15px;
}
.notification-prompt-text p {
font-size: 13px;
}
.enable-button {
padding: 6px 12px;
font-size: 13px;
}
}
</style>

View File

@@ -0,0 +1,469 @@
<script lang="ts">
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import { toast } from 'svelte-sonner';
interface NotificationPreferences {
friendRequests: boolean;
friendAccepted: boolean;
findLiked: boolean;
findCommented: boolean;
pushEnabled: boolean;
}
let preferences = $state<NotificationPreferences>({
friendRequests: true,
friendAccepted: true,
findLiked: true,
findCommented: true,
pushEnabled: true
});
let isLoading = $state<boolean>(true);
let isSaving = $state<boolean>(false);
let isSubscribing = $state<boolean>(false);
let browserPermission = $state<NotificationPermission>('default');
onMount(() => {
if (!browser) return;
loadPreferences();
checkBrowserPermission();
});
function checkBrowserPermission() {
if (!browser || !('Notification' in window)) {
browserPermission = 'denied';
return;
}
browserPermission = Notification.permission;
}
async function requestBrowserPermission() {
if (!browser || !('Notification' in window)) {
toast.error('Notifications are not supported in this browser');
return;
}
try {
isSubscribing = true;
const permission = await Notification.requestPermission();
browserPermission = permission;
if (permission === 'granted') {
// Subscribe to push notifications
await subscribeToPush();
toast.success('Notifications enabled successfully');
} else if (permission === 'denied') {
toast.error('Notification permission denied');
}
} catch (error) {
console.error('Error requesting notification permission:', error);
toast.error('Failed to enable notifications');
} finally {
isSubscribing = false;
}
}
async function subscribeToPush() {
try {
const registration = await navigator.serviceWorker.ready;
const vapidPublicKey = await fetch('/api/notifications/subscribe').then((r) => r.text());
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
});
await fetch('/api/notifications/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
} catch (error) {
console.error('Error subscribing to push:', error);
throw error;
}
}
function urlBase64ToUint8Array(base64String: string) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
async function loadPreferences() {
try {
isLoading = true;
const response = await fetch('/api/notifications/preferences');
if (!response.ok) {
throw new Error('Failed to load notification preferences');
}
const data = await response.json();
preferences = data;
} catch (error) {
console.error('Error loading notification preferences:', error);
toast.error('Failed to load notification preferences');
} finally {
isLoading = false;
}
}
async function savePreferences() {
try {
isSaving = true;
const response = await fetch('/api/notifications/preferences', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(preferences)
});
if (!response.ok) {
throw new Error('Failed to save notification preferences');
}
toast.success('Notification preferences updated');
} catch (error) {
console.error('Error saving notification preferences:', error);
toast.error('Failed to save notification preferences');
} finally {
isSaving = false;
}
}
function handleToggle(key: keyof NotificationPreferences) {
preferences[key] = !preferences[key];
savePreferences();
}
const canTogglePreferences = $derived(browserPermission === 'granted' && preferences.pushEnabled);
</script>
<div class="notification-settings">
{#if isLoading}
<div class="loading">Loading preferences...</div>
{:else}
<!-- Browser Permission Banner -->
{#if browserPermission !== 'granted'}
<div class="permission-banner {browserPermission === 'denied' ? 'denied' : 'default'}">
<div class="permission-info">
{#if browserPermission === 'denied'}
<strong>Browser notifications blocked</strong>
<p>
Please enable notifications in your browser settings to receive push notifications
</p>
{:else}
<strong>Browser permission required</strong>
<p>Enable browser notifications to receive push notifications</p>
{/if}
</div>
{#if browserPermission === 'default'}
<button class="enable-button" onclick={requestBrowserPermission} disabled={isSubscribing}>
{isSubscribing ? 'Enabling...' : 'Enable'}
</button>
{/if}
</div>
{/if}
<div class="settings-list">
<!-- Push Notifications Toggle -->
<div class="setting-item">
<div class="setting-info">
<h3>Push Notifications</h3>
<p>Enable or disable all push notifications</p>
</div>
<label class="toggle">
<input
type="checkbox"
checked={preferences.pushEnabled}
onchange={() => handleToggle('pushEnabled')}
disabled={isSaving || browserPermission !== 'granted'}
/>
<span class="toggle-slider"></span>
</label>
</div>
<div class="divider"></div>
<!-- Friend Requests -->
<div class="setting-item" class:disabled={!canTogglePreferences}>
<div class="setting-info">
<h3>Friend Requests</h3>
<p>Get notified when someone sends you a friend request</p>
</div>
<label class="toggle">
<input
type="checkbox"
checked={preferences.friendRequests}
onchange={() => handleToggle('friendRequests')}
disabled={isSaving || !canTogglePreferences}
/>
<span class="toggle-slider"></span>
</label>
</div>
<!-- Friend Accepted -->
<div class="setting-item" class:disabled={!canTogglePreferences}>
<div class="setting-info">
<h3>Friend Request Accepted</h3>
<p>Get notified when someone accepts your friend request</p>
</div>
<label class="toggle">
<input
type="checkbox"
checked={preferences.friendAccepted}
onchange={() => handleToggle('friendAccepted')}
disabled={isSaving || !canTogglePreferences}
/>
<span class="toggle-slider"></span>
</label>
</div>
<!-- Find Liked -->
<div class="setting-item" class:disabled={!canTogglePreferences}>
<div class="setting-info">
<h3>Find Likes</h3>
<p>Get notified when someone likes your find</p>
</div>
<label class="toggle">
<input
type="checkbox"
checked={preferences.findLiked}
onchange={() => handleToggle('findLiked')}
disabled={isSaving || !canTogglePreferences}
/>
<span class="toggle-slider"></span>
</label>
</div>
<!-- Find Commented -->
<div class="setting-item" class:disabled={!canTogglePreferences}>
<div class="setting-info">
<h3>Find Comments</h3>
<p>Get notified when someone comments on your find</p>
</div>
<label class="toggle">
<input
type="checkbox"
checked={preferences.findCommented}
onchange={() => handleToggle('findCommented')}
disabled={isSaving || !canTogglePreferences}
/>
<span class="toggle-slider"></span>
</label>
</div>
</div>
{/if}
</div>
<style>
.notification-settings {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family:
system-ui,
-apple-system,
sans-serif;
}
.loading {
text-align: center;
padding: 40px;
color: #6b7280;
}
.permission-banner {
background: #fef3c7;
border: 1px solid #fbbf24;
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.permission-banner.denied {
background: #fee2e2;
border-color: #ef4444;
}
.permission-info {
flex: 1;
}
.permission-info strong {
display: block;
font-size: 14px;
font-weight: 600;
color: #111827;
margin-bottom: 4px;
}
.permission-info p {
margin: 0;
font-size: 13px;
color: #6b7280;
}
.enable-button {
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
transition: background 0.2s;
}
.enable-button:hover:not(:disabled) {
background: #2563eb;
}
.enable-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.settings-list {
background: white;
border-radius: 12px;
border: 1px solid #e5e7eb;
overflow: hidden;
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
gap: 16px;
transition: opacity 0.2s;
}
.setting-item.disabled {
opacity: 0.5;
}
.setting-info {
flex: 1;
min-width: 0;
}
.setting-info h3 {
margin: 0 0 4px 0;
font-size: 16px;
font-family: inherit;
font-weight: 500;
color: #111827;
}
.setting-info p {
margin: 0;
font-size: 14px;
color: #6b7280;
line-height: 1.4;
}
.divider {
height: 1px;
background: #e5e7eb;
margin: 0 20px;
}
/* Toggle Switch */
.toggle {
position: relative;
display: inline-block;
width: 48px;
height: 28px;
flex-shrink: 0;
cursor: pointer;
}
.toggle input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #d1d5db;
border-radius: 28px;
transition: background-color 0.2s;
}
.toggle-slider:before {
position: absolute;
content: '';
height: 20px;
width: 20px;
left: 4px;
bottom: 4px;
background-color: white;
border-radius: 50%;
transition: transform 0.2s;
}
.toggle input:checked + .toggle-slider {
background-color: #3b82f6;
}
.toggle input:checked + .toggle-slider:before {
transform: translateX(20px);
}
.toggle input:disabled + .toggle-slider {
cursor: not-allowed;
opacity: 0.6;
}
@media (max-width: 768px) {
.notification-settings {
padding: 16px;
}
.settings-header h2 {
font-size: 20px;
}
.setting-item {
padding: 16px;
}
.setting-info h3 {
font-size: 15px;
}
.setting-info p {
font-size: 13px;
}
.permission-banner {
flex-direction: column;
align-items: flex-start;
}
.enable-button {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from './sheet/index.js';
import NotificationSettings from './NotificationSettings.svelte';
interface Props {
onClose: () => void;
}
let { onClose }: Props = $props();
</script>
<Sheet open={true} {onClose}>
<SheetContent class="notification-settings-sheet">
<SheetHeader>
<SheetTitle>Notifications</SheetTitle>
<SheetDescription>Manage your notification preferences</SheetDescription>
</SheetHeader>
<div class="notification-settings-content">
<NotificationSettings />
</div>
</SheetContent>
</Sheet>
<style>
:global(.notification-settings-sheet) {
max-width: 500px;
font-family:
system-ui,
-apple-system,
sans-serif;
}
.notification-settings-content {
margin-top: 20px;
}
</style>

View File

@@ -10,6 +10,7 @@
import { Skeleton } from './skeleton';
import ProfilePicture from './ProfilePicture.svelte';
import ProfilePictureSheet from './ProfilePictureSheet.svelte';
import NotificationSettingsSheet from './NotificationSettingsSheet.svelte';
interface Props {
username: string;
@@ -21,6 +22,7 @@
let { username, id, profilePictureUrl, loading = false }: Props = $props();
let showProfilePictureSheet = $state(false);
let showNotificationSettingsSheet = $state(false);
function openProfilePictureSheet() {
showProfilePictureSheet = true;
@@ -29,6 +31,14 @@
function closeProfilePictureSheet() {
showProfilePictureSheet = false;
}
function openNotificationSettingsSheet() {
showNotificationSettingsSheet = true;
}
function closeNotificationSettingsSheet() {
showNotificationSettingsSheet = false;
}
</script>
<DropdownMenu>
@@ -74,6 +84,10 @@
<a href="/friends" class="friends-link">Friends</a>
</DropdownMenuItem>
<DropdownMenuItem class="notification-settings-item" onclick={openNotificationSettingsSheet}>
Notifications
</DropdownMenuItem>
<DropdownMenuSeparator />
<div class="user-info-item">
@@ -106,6 +120,10 @@
/>
{/if}
{#if showNotificationSettingsSheet}
<NotificationSettingsSheet onClose={closeNotificationSettingsSheet} />
{/if}
<style>
:global(.profile-trigger) {
background: none;
@@ -188,6 +206,16 @@
background: #f5f5f5;
}
:global(.notification-settings-item) {
cursor: pointer;
font-weight: 500;
color: #333;
}
:global(.notification-settings-item:hover) {
background: #f5f5f5;
}
.friends-link {
display: block;
width: 100%;

View File

@@ -10,6 +10,10 @@ export { default as Modal } from './components/Modal.svelte';
export { default as Map } from './components/Map.svelte';
export { default as LocationButton } from './components/LocationButton.svelte';
export { default as LocationManager } from './components/LocationManager.svelte';
export { default as NotificationManager } from './components/NotificationManager.svelte';
export { default as NotificationPrompt } from './components/NotificationPrompt.svelte';
export { default as NotificationSettings } from './components/NotificationSettings.svelte';
export { default as NotificationSettingsSheet } from './components/NotificationSettingsSheet.svelte';
export { default as FindCard } from './components/FindCard.svelte';
export { default as FindsList } from './components/FindsList.svelte';

View File

@@ -0,0 +1,93 @@
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { db } from '$lib/server/db';
import { notificationPreferences } from '$lib/server/db/schema';
import { eq } from 'drizzle-orm';
// GET - Fetch user's notification preferences
export const GET: RequestHandler = async ({ locals }) => {
if (!locals.user) {
throw error(401, 'Unauthorized');
}
try {
// Get user's preferences
const [preferences] = await db
.select()
.from(notificationPreferences)
.where(eq(notificationPreferences.userId, locals.user.id))
.limit(1);
// If no preferences exist, return defaults
if (!preferences) {
return json({
friendRequests: true,
friendAccepted: true,
findLiked: true,
findCommented: true,
pushEnabled: true
});
}
return json({
friendRequests: preferences.friendRequests,
friendAccepted: preferences.friendAccepted,
findLiked: preferences.findLiked,
findCommented: preferences.findCommented,
pushEnabled: preferences.pushEnabled
});
} catch (err) {
console.error('Error fetching notification preferences:', err);
throw error(500, 'Failed to fetch notification preferences');
}
};
// POST - Update user's notification preferences
export const POST: RequestHandler = async ({ locals, request }) => {
if (!locals.user) {
throw error(401, 'Unauthorized');
}
try {
const body = await request.json();
const { friendRequests, friendAccepted, findLiked, findCommented, pushEnabled } = body;
// Validate boolean values
const preferences = {
friendRequests: typeof friendRequests === 'boolean' ? friendRequests : true,
friendAccepted: typeof friendAccepted === 'boolean' ? friendAccepted : true,
findLiked: typeof findLiked === 'boolean' ? findLiked : true,
findCommented: typeof findCommented === 'boolean' ? findCommented : true,
pushEnabled: typeof pushEnabled === 'boolean' ? pushEnabled : true
};
// Check if preferences exist
const [existing] = await db
.select()
.from(notificationPreferences)
.where(eq(notificationPreferences.userId, locals.user.id))
.limit(1);
if (existing) {
// Update existing preferences
await db
.update(notificationPreferences)
.set({
...preferences,
updatedAt: new Date()
})
.where(eq(notificationPreferences.userId, locals.user.id));
} else {
// Create new preferences
await db.insert(notificationPreferences).values({
userId: locals.user.id,
...preferences
});
}
return json({ success: true, preferences });
} catch (err) {
console.error('Error updating notification preferences:', err);
throw error(500, 'Failed to update notification preferences');
}
};