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:
@@ -59,6 +59,7 @@
|
|||||||
"@node-rs/argon2": "^2.0.2",
|
"@node-rs/argon2": "^2.0.2",
|
||||||
"@sveltejs/adapter-vercel": "^5.10.2",
|
"@sveltejs/adapter-vercel": "^5.10.2",
|
||||||
"arctic": "^3.7.0",
|
"arctic": "^3.7.0",
|
||||||
|
"lucide-svelte": "^0.553.0",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"postgres": "^3.4.5",
|
"postgres": "^3.4.5",
|
||||||
"sharp": "^0.34.4",
|
"sharp": "^0.34.4",
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||||||
"font-src 'self' fonts.gstatic.com; " +
|
"font-src 'self' fonts.gstatic.com; " +
|
||||||
"img-src 'self' data: blob: *.openstreetmap.org *.tile.openstreetmap.org *.r2.cloudflarestorage.com *.r2.dev; " +
|
"img-src 'self' data: blob: *.openstreetmap.org *.tile.openstreetmap.org *.r2.cloudflarestorage.com *.r2.dev; " +
|
||||||
"media-src 'self' *.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'; " +
|
"frame-ancestors 'none'; " +
|
||||||
"base-uri 'self'; " +
|
"base-uri 'self'; " +
|
||||||
"form-action 'self';"
|
"form-action 'self';"
|
||||||
|
|||||||
@@ -1,32 +1,33 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
import NotificationPrompt from './NotificationPrompt.svelte';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NotificationManager - Handles push notification subscription
|
* 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 permissionStatus = $state<NotificationPermission>('default');
|
||||||
let subscriptionStatus = $state<'idle' | 'subscribing' | 'subscribed' | 'error'>('idle');
|
let subscriptionStatus = $state<'idle' | 'subscribing' | 'subscribed' | 'error'>('idle');
|
||||||
let errorMessage = $state<string>('');
|
let errorMessage = $state<string>('');
|
||||||
|
let showPrompt = $state<boolean>(false);
|
||||||
|
let isSupported = $state<boolean>(false);
|
||||||
|
|
||||||
|
const PROMPT_DISMISSED_KEY = 'notification-prompt-dismissed';
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
|
|
||||||
// Check if notifications are supported
|
// Check if notifications and service workers are supported
|
||||||
if (!('Notification' in window)) {
|
isSupported = 'Notification' in window && 'serviceWorker' in navigator;
|
||||||
console.log('This browser does not support notifications');
|
|
||||||
|
if (!isSupported) {
|
||||||
|
console.log('Notifications or service workers not supported in this browser');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if service workers are supported
|
// Initialize without requesting permission
|
||||||
if (!('serviceWorker' in navigator)) {
|
|
||||||
console.log('This browser does not support service workers');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize notification subscription
|
|
||||||
initializeNotifications();
|
initializeNotifications();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -37,60 +38,93 @@
|
|||||||
// Get current permission status
|
// Get current permission status
|
||||||
permissionStatus = Notification.permission;
|
permissionStatus = Notification.permission;
|
||||||
console.log('[NotificationManager] Permission status:', permissionStatus);
|
console.log('[NotificationManager] Permission status:', permissionStatus);
|
||||||
// If already denied, don't do anything
|
|
||||||
if (permissionStatus === 'denied') {
|
// If already granted, subscribe automatically
|
||||||
console.log('Notification permission denied by user');
|
if (permissionStatus === 'granted') {
|
||||||
return;
|
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();
|
let registration = await navigator.serviceWorker.getRegistration();
|
||||||
|
|
||||||
// If no registration exists, register it
|
|
||||||
if (!registration) {
|
if (!registration) {
|
||||||
console.log('[NotificationManager] No SW found, registering...');
|
console.log('[NotificationManager] No SW found, registering...');
|
||||||
registration = await navigator.serviceWorker.register('/service-worker.js', {
|
registration = await navigator.serviceWorker.register('/service-worker.js', {
|
||||||
type: 'module'
|
type: 'module'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for service worker to be ready
|
// Wait for service worker to be ready
|
||||||
await navigator.serviceWorker.ready;
|
await navigator.serviceWorker.ready;
|
||||||
console.log('[NotificationManager] Service worker 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
|
// Get VAPID public key from server
|
||||||
console.log('[NotificationManager] Fetching VAPID key...');
|
console.log('[NotificationManager] Fetching VAPID key...');
|
||||||
const response = await fetch('/api/notifications/subscribe');
|
const response = await fetch('/api/notifications/subscribe');
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to get VAPID public key');
|
throw new Error('Failed to get VAPID public key');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { publicKey } = await response.json();
|
const { publicKey } = await response.json();
|
||||||
console.log('[NotificationManager] Got VAPID key:', publicKey);
|
console.log('[NotificationManager] Got VAPID key:', publicKey);
|
||||||
|
|
||||||
// Check if already subscribed
|
// Check if already subscribed
|
||||||
console.log('[NotificationManager] Checking existing subscription...');
|
console.log('[NotificationManager] Checking existing subscription...');
|
||||||
let subscription = await registration.pushManager.getSubscription();
|
let subscription = await registration.pushManager.getSubscription();
|
||||||
console.log('[NotificationManager] Existing subscription:', subscription);
|
console.log('[NotificationManager] Existing subscription:', subscription);
|
||||||
|
|
||||||
// If not subscribed, create new subscription
|
// If not subscribed, create new subscription
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
console.log('[NotificationManager] Creating new subscription...');
|
console.log('[NotificationManager] Creating new subscription...');
|
||||||
@@ -100,6 +134,7 @@
|
|||||||
});
|
});
|
||||||
console.log('[NotificationManager] Subscription created:', subscription);
|
console.log('[NotificationManager] Subscription created:', subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send subscription to server
|
// Send subscription to server
|
||||||
console.log('[NotificationManager] Sending subscription to server...');
|
console.log('[NotificationManager] Sending subscription to server...');
|
||||||
const saveResponse = await fetch('/api/notifications/subscribe', {
|
const saveResponse = await fetch('/api/notifications/subscribe', {
|
||||||
@@ -117,12 +152,15 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[NotificationManager] Save response status:', saveResponse.status);
|
console.log('[NotificationManager] Save response status:', saveResponse.status);
|
||||||
|
|
||||||
if (!saveResponse.ok) {
|
if (!saveResponse.ok) {
|
||||||
const errorText = await saveResponse.text();
|
const errorText = await saveResponse.text();
|
||||||
console.error('[NotificationManager] Save failed:', errorText);
|
console.error('[NotificationManager] Save failed:', errorText);
|
||||||
throw new Error('Failed to save subscription to server');
|
throw new Error('Failed to save subscription to server');
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptionStatus = 'subscribed';
|
subscriptionStatus = 'subscribed';
|
||||||
console.log('[NotificationManager] Successfully subscribed to push notifications!');
|
console.log('[NotificationManager] Successfully subscribed to push notifications!');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -162,3 +200,7 @@
|
|||||||
return window.btoa(binary);
|
return window.btoa(binary);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if showPrompt}
|
||||||
|
<NotificationPrompt onEnable={handleEnableNotifications} onDismiss={handleDismissPrompt} />
|
||||||
|
{/if}
|
||||||
|
|||||||
173
src/lib/components/NotificationPrompt.svelte
Normal file
173
src/lib/components/NotificationPrompt.svelte
Normal 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>
|
||||||
469
src/lib/components/NotificationSettings.svelte
Normal file
469
src/lib/components/NotificationSettings.svelte
Normal 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>
|
||||||
36
src/lib/components/NotificationSettingsSheet.svelte
Normal file
36
src/lib/components/NotificationSettingsSheet.svelte
Normal 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>
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
import { Skeleton } from './skeleton';
|
import { Skeleton } from './skeleton';
|
||||||
import ProfilePicture from './ProfilePicture.svelte';
|
import ProfilePicture from './ProfilePicture.svelte';
|
||||||
import ProfilePictureSheet from './ProfilePictureSheet.svelte';
|
import ProfilePictureSheet from './ProfilePictureSheet.svelte';
|
||||||
|
import NotificationSettingsSheet from './NotificationSettingsSheet.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
let { username, id, profilePictureUrl, loading = false }: Props = $props();
|
let { username, id, profilePictureUrl, loading = false }: Props = $props();
|
||||||
|
|
||||||
let showProfilePictureSheet = $state(false);
|
let showProfilePictureSheet = $state(false);
|
||||||
|
let showNotificationSettingsSheet = $state(false);
|
||||||
|
|
||||||
function openProfilePictureSheet() {
|
function openProfilePictureSheet() {
|
||||||
showProfilePictureSheet = true;
|
showProfilePictureSheet = true;
|
||||||
@@ -29,6 +31,14 @@
|
|||||||
function closeProfilePictureSheet() {
|
function closeProfilePictureSheet() {
|
||||||
showProfilePictureSheet = false;
|
showProfilePictureSheet = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openNotificationSettingsSheet() {
|
||||||
|
showNotificationSettingsSheet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeNotificationSettingsSheet() {
|
||||||
|
showNotificationSettingsSheet = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -74,6 +84,10 @@
|
|||||||
<a href="/friends" class="friends-link">Friends</a>
|
<a href="/friends" class="friends-link">Friends</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem class="notification-settings-item" onclick={openNotificationSettingsSheet}>
|
||||||
|
Notifications
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<div class="user-info-item">
|
<div class="user-info-item">
|
||||||
@@ -106,6 +120,10 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if showNotificationSettingsSheet}
|
||||||
|
<NotificationSettingsSheet onClose={closeNotificationSettingsSheet} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:global(.profile-trigger) {
|
:global(.profile-trigger) {
|
||||||
background: none;
|
background: none;
|
||||||
@@ -188,6 +206,16 @@
|
|||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.notification-settings-item) {
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.notification-settings-item:hover) {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
.friends-link {
|
.friends-link {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ export { default as Modal } from './components/Modal.svelte';
|
|||||||
export { default as Map } from './components/Map.svelte';
|
export { default as Map } from './components/Map.svelte';
|
||||||
export { default as LocationButton } from './components/LocationButton.svelte';
|
export { default as LocationButton } from './components/LocationButton.svelte';
|
||||||
export { default as LocationManager } from './components/LocationManager.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 FindCard } from './components/FindCard.svelte';
|
||||||
export { default as FindsList } from './components/FindsList.svelte';
|
export { default as FindsList } from './components/FindsList.svelte';
|
||||||
|
|
||||||
|
|||||||
93
src/routes/api/notifications/preferences/+server.ts
Normal file
93
src/routes/api/notifications/preferences/+server.ts
Normal 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');
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user