feat: implement notification system

This commit is contained in:
Face 2025-06-11 18:37:03 +03:00
parent de3f8a4929
commit e61c41e414
19 changed files with 883 additions and 3196 deletions

View file

@ -0,0 +1,61 @@
import { writable, derived } from 'svelte/store';
export interface Notification {
id: number;
type: string;
title: string;
message: string;
data: any;
isRead: boolean;
createdAt: string;
}
export const NOTIFICATIONS = writable<Notification[]>([]);
export const UNREAD_COUNT = writable<number>(0);
export async function fetchNotifications(unreadOnly = false) {
try {
const params = new URLSearchParams({
unread_only: unreadOnly.toString()
});
const response = await fetch(`/api/notifications?${params}`);
if (!response.ok) throw new Error('Failed to fetch notifications');
const data = await response.json();
NOTIFICATIONS.set(data.notifications);
UNREAD_COUNT.set(data.unreadCount);
return data;
} catch (error) {
console.error('Failed to fetch notifications:', error);
throw error;
}
}
export async function markNotificationsAsRead(ids: number[]) {
try {
const response = await fetch('/api/notifications', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids, markAsRead: true })
});
if (!response.ok) throw new Error('Failed to mark notifications as read');
NOTIFICATIONS.update(notifications =>
notifications.map(notif =>
ids.includes(notif.id) ? { ...notif, isRead: true } : notif
)
);
UNREAD_COUNT.update(count => Math.max(0, count - ids.length));
} catch (error) {
console.error('Failed to mark notifications as read:', error);
throw error;
}
}
export const hasUnreadNotifications = derived(UNREAD_COUNT, count => count > 0);

View file

@ -1,6 +1,10 @@
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
import { PUBLIC_WEBSOCKET_URL } from '$env/static/public';
import { NOTIFICATIONS, UNREAD_COUNT } from './notifications';
import { USER_DATA } from './user-data';
import { toast } from 'svelte-sonner';
import { goto } from '$app/navigation';
export interface LiveTrade {
type: 'BUY' | 'SELL' | 'TRANSFER_IN' | 'TRANSFER_OUT';
@ -187,6 +191,32 @@ function handleWebSocketMessage(event: MessageEvent): void {
handleCommentMessage(message);
break;
case 'notification':
const notification = {
id: Date.now(),
type: message.notificationType,
title: message.title,
message: message.message,
isRead: false,
createdAt: message.timestamp,
data: message.amount ? { amount: message.amount } : null
};
NOTIFICATIONS.update(notifications => [notification, ...notifications]);
UNREAD_COUNT.update(count => count + 1);
toast.success(message.title, {
description: message.message,
action: {
label: 'View',
onClick: () => {
goto('/notifications');
}
},
duration: 5000
});
break;
default:
console.log('Unhandled message type:', message.type, message);
}
@ -267,13 +297,56 @@ function unsubscribeFromPriceUpdates(coinSymbol: string): void {
priceUpdateSubscriptions.delete(coinSymbol);
}
export const websocketController = {
connect,
disconnect,
setCoin,
subscribeToComments,
unsubscribeFromComments,
subscribeToPriceUpdates,
unsubscribeFromPriceUpdates,
loadInitialTrades
};
class WebSocketController {
connect() {
connect();
}
disconnect() {
disconnect();
}
setCoin(coinSymbol: string) {
setCoin(coinSymbol);
}
subscribeToComments(coinSymbol: string, callback: (message: any) => void) {
subscribeToComments(coinSymbol, callback);
}
unsubscribeFromComments(coinSymbol: string) {
unsubscribeFromComments(coinSymbol);
}
subscribeToPriceUpdates(coinSymbol: string, callback: (priceUpdate: PriceUpdate) => void) {
subscribeToPriceUpdates(coinSymbol, callback);
}
unsubscribeFromPriceUpdates(coinSymbol: string) {
unsubscribeFromPriceUpdates(coinSymbol);
}
loadInitialTrades(mode: 'preview' | 'expanded' = 'preview') {
loadInitialTrades(mode);
}
setUser(userId: string) {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
type: 'set_user',
userId
}));
}
}
}
// Auto-connect user when USER_DATA changes
if (typeof window !== 'undefined') {
USER_DATA.subscribe(user => {
if (user?.id) {
websocketController.setUser(user.id.toString());
}
});
}
export const websocketController = new WebSocketController();