This commit is contained in:
Stijnus
2025-01-21 11:55:26 +01:00
parent 436a8e54bf
commit 78d4e1bb54
13 changed files with 2011 additions and 548 deletions

View File

@@ -1,40 +1,94 @@
import { logStore, type LogEntry } from '~/lib/stores/logs';
export type NotificationType = 'info' | 'warning' | 'error' | 'success' | 'update';
export interface NotificationDetails {
type?: string;
message?: string;
currentVersion?: string;
latestVersion?: string;
branch?: string;
updateUrl?: string;
}
export interface Notification {
id: string;
title: string;
message: string;
type: 'info' | 'warning' | 'error' | 'success';
type: NotificationType;
read: boolean;
timestamp: string;
details?: NotificationDetails;
}
interface LogEntryWithRead extends LogEntry {
read?: boolean;
}
const mapLogToNotification = (log: LogEntryWithRead): Notification => {
const type: NotificationType =
log.details?.type === 'update'
? 'update'
: log.level === 'error'
? 'error'
: log.level === 'warning'
? 'warning'
: 'info';
const baseNotification: Notification = {
id: log.id,
title: log.category.charAt(0).toUpperCase() + log.category.slice(1),
message: log.message,
type,
read: log.read || false,
timestamp: log.timestamp,
};
if (log.details) {
return {
...baseNotification,
details: log.details as NotificationDetails,
};
}
return baseNotification;
};
export const getNotifications = async (): Promise<Notification[]> => {
/*
* TODO: Implement actual notifications logic
* This is a mock implementation
*/
return [
{
id: 'notif-1',
title: 'Welcome to Bolt',
message: 'Get started by exploring the features',
type: 'info',
read: true,
timestamp: new Date().toISOString(),
},
{
id: 'notif-2',
title: 'New Update Available',
message: 'Version 1.0.1 is now available',
type: 'info',
read: false,
timestamp: new Date().toISOString(),
},
];
const logs = Object.values(logStore.logs.get()) as LogEntryWithRead[];
return logs
.filter((log) => {
if (log.details?.type === 'update') {
return true;
}
return log.level === 'error' || log.level === 'warning';
})
.map(mapLogToNotification)
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
};
export const markNotificationRead = async (notificationId: string): Promise<void> => {
/*
* TODO: Implement actual notification read logic
*/
console.log(`Marking notification ${notificationId} as read`);
logStore.markAsRead(notificationId);
};
export const clearNotifications = async (): Promise<void> => {
logStore.clearLogs();
};
export const getUnreadCount = (): number => {
const logs = Object.values(logStore.logs.get()) as LogEntryWithRead[];
return logs.filter((log) => {
if (!log.read) {
if (log.details?.type === 'update') {
return true;
}
return log.level === 'error' || log.level === 'warning';
}
return false;
}).length;
};

View File

@@ -1,34 +1,17 @@
import { useState, useEffect } from 'react';
import { getNotifications, markNotificationRead, type Notification } from '~/lib/api/notifications';
const READ_NOTIFICATIONS_KEY = 'bolt_read_notifications';
const getReadNotifications = (): string[] => {
try {
const stored = localStorage.getItem(READ_NOTIFICATIONS_KEY);
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
};
const setReadNotifications = (notificationIds: string[]) => {
try {
localStorage.setItem(READ_NOTIFICATIONS_KEY, JSON.stringify(notificationIds));
} catch (error) {
console.error('Failed to persist read notifications:', error);
}
};
import { logStore } from '~/lib/stores/logs';
import { useStore } from '@nanostores/react';
export const useNotifications = () => {
const [hasUnreadNotifications, setHasUnreadNotifications] = useState(false);
const [unreadNotifications, setUnreadNotifications] = useState<Notification[]>([]);
const [readNotificationIds, setReadNotificationIds] = useState<string[]>(() => getReadNotifications());
const logs = useStore(logStore.logs);
const checkNotifications = async () => {
try {
const notifications = await getNotifications();
const unread = notifications.filter((n) => !readNotificationIds.includes(n.id));
const unread = notifications.filter((n) => !logStore.isRead(n.id));
setUnreadNotifications(unread);
setHasUnreadNotifications(unread.length > 0);
} catch (error) {
@@ -43,17 +26,12 @@ export const useNotifications = () => {
const interval = setInterval(checkNotifications, 60 * 1000);
return () => clearInterval(interval);
}, [readNotificationIds]);
}, [logs]); // Re-run when logs change
const markAsRead = async (notificationId: string) => {
try {
await markNotificationRead(notificationId);
const newReadIds = [...readNotificationIds, notificationId];
setReadNotificationIds(newReadIds);
setReadNotifications(newReadIds);
setUnreadNotifications((prev) => prev.filter((n) => n.id !== notificationId));
setHasUnreadNotifications(unreadNotifications.length > 1);
await checkNotifications();
} catch (error) {
console.error('Failed to mark notification as read:', error);
}
@@ -61,13 +39,9 @@ export const useNotifications = () => {
const markAllAsRead = async () => {
try {
await Promise.all(unreadNotifications.map((n) => markNotificationRead(n.id)));
const newReadIds = [...readNotificationIds, ...unreadNotifications.map((n) => n.id)];
setReadNotificationIds(newReadIds);
setReadNotifications(newReadIds);
setUnreadNotifications([]);
setHasUnreadNotifications(false);
const notifications = await getNotifications();
await Promise.all(notifications.map((n) => markNotificationRead(n.id)));
await checkNotifications();
} catch (error) {
console.error('Failed to mark all notifications as read:', error);
}

View File

@@ -19,12 +19,25 @@ export interface LogEntry {
const MAX_LOGS = 1000; // Maximum number of logs to keep in memory
class LogStore {
logInfo(message: string, details: { type: string; message: string }) {
return this.addLog(message, 'info', 'system', details);
}
logSuccess(message: string, details: { type: string; message: string }) {
return this.addLog(message, 'info', 'system', { ...details, success: true });
}
private _logs = map<Record<string, LogEntry>>({});
showLogs = atom(true);
private _readLogs = new Set<string>();
constructor() {
// Load saved logs from cookies on initialization
this._loadLogs();
// Only load read logs in browser environment
if (typeof window !== 'undefined') {
this._loadReadLogs();
}
}
// Expose the logs store for subscription
@@ -45,11 +58,36 @@ class LogStore {
}
}
private _loadReadLogs() {
if (typeof window === 'undefined') {
return;
}
const savedReadLogs = localStorage.getItem('bolt_read_logs');
if (savedReadLogs) {
try {
const parsedReadLogs = JSON.parse(savedReadLogs);
this._readLogs = new Set(parsedReadLogs);
} catch (error) {
logger.error('Failed to parse read logs:', error);
}
}
}
private _saveLogs() {
const currentLogs = this._logs.get();
Cookies.set('eventLogs', JSON.stringify(currentLogs));
}
private _saveReadLogs() {
if (typeof window === 'undefined') {
return;
}
localStorage.setItem('bolt_read_logs', JSON.stringify(Array.from(this._readLogs)));
}
private _generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
@@ -210,6 +248,20 @@ class LogStore {
return matchesLevel && matchesCategory && matchesSearch;
});
}
markAsRead(logId: string) {
this._readLogs.add(logId);
this._saveReadLogs();
}
isRead(logId: string): boolean {
return this._readLogs.has(logId);
}
clearReadLogs() {
this._readLogs.clear();
this._saveReadLogs();
}
}
export const logStore = new LogStore();