Files
fotospiel-app/resources/js/guest/context/NotificationCenterContext.tsx
2025-11-12 16:56:50 +01:00

222 lines
5.6 KiB
TypeScript

import React from 'react';
import { useUploadQueue } from '../queue/hooks';
import type { QueueItem } from '../queue/queue';
import {
dismissGuestNotification,
fetchGuestNotifications,
markGuestNotificationRead,
type GuestNotificationItem,
} from '../services/notificationApi';
export type NotificationCenterValue = {
notifications: GuestNotificationItem[];
unreadCount: number;
queueItems: QueueItem[];
queueCount: number;
totalCount: number;
loading: boolean;
refresh: () => Promise<void>;
markAsRead: (id: number) => Promise<void>;
dismiss: (id: number) => Promise<void>;
eventToken: string;
};
const NotificationCenterContext = React.createContext<NotificationCenterValue | null>(null);
export function NotificationCenterProvider({ eventToken, children }: { eventToken: string; children: React.ReactNode }) {
const { items, loading: queueLoading, refresh: refreshQueue } = useUploadQueue();
const [notifications, setNotifications] = React.useState<GuestNotificationItem[]>([]);
const [unreadCount, setUnreadCount] = React.useState(0);
const [loadingNotifications, setLoadingNotifications] = React.useState(true);
const etagRef = React.useRef<string | null>(null);
const fetchLockRef = React.useRef(false);
const queueCount = React.useMemo(
() => items.filter((item) => item.status !== 'done').length,
[items]
);
const loadNotifications = React.useCallback(
async (options: { silent?: boolean } = {}) => {
if (!eventToken) {
if (!options.silent) {
setLoadingNotifications(false);
}
return;
}
if (fetchLockRef.current) {
return;
}
fetchLockRef.current = true;
if (!options.silent) {
setLoadingNotifications(true);
}
try {
const result = await fetchGuestNotifications(eventToken, etagRef.current);
if (!result.notModified) {
setNotifications(result.notifications);
setUnreadCount(result.unreadCount);
}
etagRef.current = result.etag;
} catch (error) {
console.error('Failed to load guest notifications', error);
if (!options.silent) {
setNotifications([]);
setUnreadCount(0);
}
} finally {
fetchLockRef.current = false;
if (!options.silent) {
setLoadingNotifications(false);
}
}
},
[eventToken]
);
React.useEffect(() => {
setNotifications([]);
setUnreadCount(0);
etagRef.current = null;
if (!eventToken) {
setLoadingNotifications(false);
return;
}
setLoadingNotifications(true);
void loadNotifications();
}, [eventToken, loadNotifications]);
React.useEffect(() => {
if (!eventToken) {
return;
}
const interval = window.setInterval(() => {
void loadNotifications({ silent: true });
}, 90000);
return () => window.clearInterval(interval);
}, [eventToken, loadNotifications]);
const markAsRead = React.useCallback(
async (id: number) => {
if (!eventToken) {
return;
}
let decremented = false;
setNotifications((prev) =>
prev.map((item) => {
if (item.id !== id) {
return item;
}
if (item.status === 'new') {
decremented = true;
}
return {
...item,
status: 'read',
readAt: new Date().toISOString(),
};
})
);
if (decremented) {
setUnreadCount((prev) => Math.max(0, prev - 1));
}
try {
await markGuestNotificationRead(eventToken, id);
} catch (error) {
console.error('Failed to mark notification as read', error);
void loadNotifications({ silent: true });
}
},
[eventToken, loadNotifications]
);
const dismiss = React.useCallback(
async (id: number) => {
if (!eventToken) {
return;
}
let decremented = false;
setNotifications((prev) =>
prev.map((item) => {
if (item.id !== id) {
return item;
}
if (item.status === 'new') {
decremented = true;
}
return {
...item,
status: 'dismissed',
dismissedAt: new Date().toISOString(),
};
})
);
if (decremented) {
setUnreadCount((prev) => Math.max(0, prev - 1));
}
try {
await dismissGuestNotification(eventToken, id);
} catch (error) {
console.error('Failed to dismiss notification', error);
void loadNotifications({ silent: true });
}
},
[eventToken, loadNotifications]
);
const refresh = React.useCallback(async () => {
await Promise.all([loadNotifications(), refreshQueue()]);
}, [loadNotifications, refreshQueue]);
const loading = loadingNotifications || queueLoading;
const totalCount = unreadCount + queueCount;
const value: NotificationCenterValue = {
notifications,
unreadCount,
queueItems: items,
queueCount,
totalCount,
loading,
refresh,
markAsRead,
dismiss,
eventToken,
};
return (
<NotificationCenterContext.Provider value={value}>
{children}
</NotificationCenterContext.Provider>
);
}
export function useNotificationCenter(): NotificationCenterValue {
const ctx = React.useContext(NotificationCenterContext);
if (!ctx) {
throw new Error('useNotificationCenter must be used within NotificationCenterProvider');
}
return ctx;
}
export function useOptionalNotificationCenter(): NotificationCenterValue | null {
return React.useContext(NotificationCenterContext);
}