feat: add guest notification center
This commit is contained in:
146
resources/js/guest/services/notificationApi.ts
Normal file
146
resources/js/guest/services/notificationApi.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { getDeviceId } from '../lib/device';
|
||||
|
||||
export type GuestNotificationCta = {
|
||||
label: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export type GuestNotificationItem = {
|
||||
id: number;
|
||||
type: string;
|
||||
title: string;
|
||||
body: string | null;
|
||||
status: 'new' | 'read' | 'dismissed';
|
||||
createdAt: string;
|
||||
readAt?: string | null;
|
||||
dismissedAt?: string | null;
|
||||
cta?: GuestNotificationCta | null;
|
||||
payload?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
export type GuestNotificationFetchResult = {
|
||||
notifications: GuestNotificationItem[];
|
||||
unreadCount: number;
|
||||
etag: string | null;
|
||||
notModified: boolean;
|
||||
};
|
||||
|
||||
type GuestNotificationResponse = {
|
||||
data?: Array<{
|
||||
id?: number | string;
|
||||
type?: string;
|
||||
title?: string;
|
||||
body?: string | null;
|
||||
status?: 'new' | 'read' | 'dismissed';
|
||||
created_at?: string;
|
||||
read_at?: string | null;
|
||||
dismissed_at?: string | null;
|
||||
cta?: GuestNotificationCta | null;
|
||||
payload?: Record<string, unknown> | null;
|
||||
}>;
|
||||
meta?: {
|
||||
unread_count?: number;
|
||||
};
|
||||
};
|
||||
|
||||
type GuestNotificationRow = NonNullable<GuestNotificationResponse['data']>[number];
|
||||
|
||||
function buildHeaders(etag?: string | null): HeadersInit {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Device-Id': getDeviceId(),
|
||||
};
|
||||
|
||||
if (etag) {
|
||||
headers['If-None-Match'] = etag;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
function mapNotification(payload: GuestNotificationRow): GuestNotificationItem {
|
||||
return {
|
||||
id: Number(payload.id ?? 0),
|
||||
type: payload.type ?? 'broadcast',
|
||||
title: payload.title ?? '',
|
||||
body: payload.body ?? null,
|
||||
status: payload.status === 'read' || payload.status === 'dismissed' ? payload.status : 'new',
|
||||
createdAt: payload.created_at ?? new Date().toISOString(),
|
||||
readAt: payload.read_at ?? null,
|
||||
dismissedAt: payload.dismissed_at ?? null,
|
||||
cta: payload.cta ?? null,
|
||||
payload: payload.payload ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchGuestNotifications(eventToken: string, etag?: string | null): Promise<GuestNotificationFetchResult> {
|
||||
const response = await fetch(`/api/v1/events/${encodeURIComponent(eventToken)}/notifications`, {
|
||||
method: 'GET',
|
||||
headers: buildHeaders(etag),
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (response.status === 304 && etag) {
|
||||
return {
|
||||
notifications: [],
|
||||
unreadCount: 0,
|
||||
etag,
|
||||
notModified: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const reason = await safeParseError(response);
|
||||
throw new Error(reason ?? 'Benachrichtigungen konnten nicht geladen werden.');
|
||||
}
|
||||
|
||||
const body = (await response.json()) as GuestNotificationResponse;
|
||||
const rows = Array.isArray(body.data) ? body.data : [];
|
||||
const notifications = rows.map(mapNotification);
|
||||
const unreadCount = typeof body.meta?.unread_count === 'number'
|
||||
? body.meta.unread_count
|
||||
: notifications.filter((item) => item.status === 'new').length;
|
||||
|
||||
return {
|
||||
notifications,
|
||||
unreadCount,
|
||||
etag: response.headers.get('ETag'),
|
||||
notModified: false,
|
||||
};
|
||||
}
|
||||
|
||||
export async function markGuestNotificationRead(eventToken: string, notificationId: number): Promise<void> {
|
||||
await postNotificationAction(eventToken, notificationId, 'read');
|
||||
}
|
||||
|
||||
export async function dismissGuestNotification(eventToken: string, notificationId: number): Promise<void> {
|
||||
await postNotificationAction(eventToken, notificationId, 'dismiss');
|
||||
}
|
||||
|
||||
async function postNotificationAction(eventToken: string, notificationId: number, action: 'read' | 'dismiss'): Promise<void> {
|
||||
const response = await fetch(`/api/v1/events/${encodeURIComponent(eventToken)}/notifications/${notificationId}/${action}`, {
|
||||
method: 'POST',
|
||||
headers: buildHeaders(),
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const reason = await safeParseError(response);
|
||||
throw new Error(reason ?? 'Aktion konnte nicht ausgeführt werden.');
|
||||
}
|
||||
}
|
||||
|
||||
async function safeParseError(response: Response): Promise<string | null> {
|
||||
try {
|
||||
const payload = await response.clone().json();
|
||||
const message = payload?.error?.message ?? payload?.message;
|
||||
if (typeof message === 'string' && message.trim() !== '') {
|
||||
return message.trim();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse notification API error', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user