Update admin PWA events, branding, and packages
This commit is contained in:
@@ -180,6 +180,63 @@ export type EventStats = {
|
||||
pending_photos?: number;
|
||||
};
|
||||
|
||||
export type EventEngagementSummary = {
|
||||
totalPhotos: number;
|
||||
uniqueGuests: number;
|
||||
tasksSolved: number;
|
||||
likesTotal: number;
|
||||
};
|
||||
|
||||
export type EventEngagementLeaderboardEntry = {
|
||||
guest: string;
|
||||
photos: number;
|
||||
likes: number;
|
||||
};
|
||||
|
||||
export type EventEngagementTopPhoto = {
|
||||
photoId: number;
|
||||
guest: string;
|
||||
likes: number;
|
||||
task?: string | null;
|
||||
createdAt: string;
|
||||
thumbnail: string | null;
|
||||
};
|
||||
|
||||
export type EventEngagementTrendingEmotion = {
|
||||
emotionId: number;
|
||||
name: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type EventEngagementTimelinePoint = {
|
||||
date: string;
|
||||
photos: number;
|
||||
guests: number;
|
||||
};
|
||||
|
||||
export type EventEngagementFeedEntry = {
|
||||
photoId: number;
|
||||
guest: string;
|
||||
task?: string | null;
|
||||
likes: number;
|
||||
createdAt: string;
|
||||
thumbnail: string | null;
|
||||
};
|
||||
|
||||
export type EventEngagement = {
|
||||
summary: EventEngagementSummary;
|
||||
leaderboards: {
|
||||
uploads: EventEngagementLeaderboardEntry[];
|
||||
likes: EventEngagementLeaderboardEntry[];
|
||||
};
|
||||
highlights: {
|
||||
topPhoto: EventEngagementTopPhoto | null;
|
||||
trendingEmotion: EventEngagementTrendingEmotion | null;
|
||||
timeline: EventEngagementTimelinePoint[];
|
||||
};
|
||||
feed: EventEngagementFeedEntry[];
|
||||
};
|
||||
|
||||
export type PhotoboothStatusMetrics = {
|
||||
uploads_last_hour?: number | null;
|
||||
uploads_today?: number | null;
|
||||
@@ -255,6 +312,7 @@ export type TenantFont = {
|
||||
export type WatermarkSettings = {
|
||||
mode?: 'base' | 'custom' | 'off';
|
||||
asset?: string | null;
|
||||
asset_url?: string | null;
|
||||
asset_data_url?: string | null;
|
||||
position?:
|
||||
| 'top-left'
|
||||
@@ -1321,6 +1379,113 @@ function normalizeQrInvite(raw: JsonValue): EventQrInvite {
|
||||
};
|
||||
}
|
||||
|
||||
function toEngagementNumber(value: unknown, fallback = 0): number {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string' && value !== '') {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function toEngagementString(value: unknown): string {
|
||||
return typeof value === 'string' ? value : '';
|
||||
}
|
||||
|
||||
function normalizeEventEngagement(payload: JsonValue): EventEngagement {
|
||||
const record = payload && typeof payload === 'object' ? (payload as Record<string, unknown>) : {};
|
||||
const summary = record.summary && typeof record.summary === 'object' ? (record.summary as Record<string, unknown>) : {};
|
||||
const leaderboards = record.leaderboards && typeof record.leaderboards === 'object'
|
||||
? (record.leaderboards as Record<string, unknown>)
|
||||
: {};
|
||||
const highlights = record.highlights && typeof record.highlights === 'object'
|
||||
? (record.highlights as Record<string, unknown>)
|
||||
: {};
|
||||
const feedRaw = Array.isArray(record.feed) ? record.feed : [];
|
||||
|
||||
const uploadsBoard = Array.isArray(leaderboards.uploads)
|
||||
? (leaderboards.uploads as Record<string, unknown>[]).map((row) => ({
|
||||
guest: toEngagementString(row.guest),
|
||||
photos: toEngagementNumber(row.photos),
|
||||
likes: toEngagementNumber(row.likes),
|
||||
}))
|
||||
: [];
|
||||
|
||||
const likesBoard = Array.isArray(leaderboards.likes)
|
||||
? (leaderboards.likes as Record<string, unknown>[]).map((row) => ({
|
||||
guest: toEngagementString(row.guest),
|
||||
photos: toEngagementNumber(row.photos),
|
||||
likes: toEngagementNumber(row.likes),
|
||||
}))
|
||||
: [];
|
||||
|
||||
const topPhotoRaw = highlights.top_photo && typeof highlights.top_photo === 'object'
|
||||
? (highlights.top_photo as Record<string, unknown>)
|
||||
: null;
|
||||
const topPhoto = topPhotoRaw
|
||||
? {
|
||||
photoId: toEngagementNumber(topPhotoRaw.photo_id),
|
||||
guest: toEngagementString(topPhotoRaw.guest),
|
||||
likes: toEngagementNumber(topPhotoRaw.likes),
|
||||
task: (topPhotoRaw as { task?: string | null }).task ?? null,
|
||||
createdAt: toEngagementString(topPhotoRaw.created_at),
|
||||
thumbnail: topPhotoRaw.thumbnail ? toEngagementString(topPhotoRaw.thumbnail) : null,
|
||||
}
|
||||
: null;
|
||||
|
||||
const trendingRaw = highlights.trending_emotion && typeof highlights.trending_emotion === 'object'
|
||||
? (highlights.trending_emotion as Record<string, unknown>)
|
||||
: null;
|
||||
const trendingEmotion = trendingRaw
|
||||
? {
|
||||
emotionId: toEngagementNumber(trendingRaw.emotion_id),
|
||||
name: toEngagementString(trendingRaw.name),
|
||||
count: toEngagementNumber(trendingRaw.count),
|
||||
}
|
||||
: null;
|
||||
|
||||
const timeline = Array.isArray(highlights.timeline)
|
||||
? (highlights.timeline as Record<string, unknown>[]).map((row) => ({
|
||||
date: toEngagementString(row.date),
|
||||
photos: toEngagementNumber(row.photos),
|
||||
guests: toEngagementNumber(row.guests),
|
||||
}))
|
||||
: [];
|
||||
|
||||
const feed = feedRaw.map((row) => {
|
||||
const entry = row as Record<string, unknown>;
|
||||
return {
|
||||
photoId: toEngagementNumber(entry.photo_id),
|
||||
guest: toEngagementString(entry.guest),
|
||||
task: (entry as { task?: string | null }).task ?? null,
|
||||
likes: toEngagementNumber(entry.likes),
|
||||
createdAt: toEngagementString(entry.created_at),
|
||||
thumbnail: entry.thumbnail ? toEngagementString(entry.thumbnail) : null,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
summary: {
|
||||
totalPhotos: toEngagementNumber(summary.total_photos),
|
||||
uniqueGuests: toEngagementNumber(summary.unique_guests),
|
||||
tasksSolved: toEngagementNumber(summary.tasks_solved),
|
||||
likesTotal: toEngagementNumber(summary.likes_total),
|
||||
},
|
||||
leaderboards: {
|
||||
uploads: uploadsBoard,
|
||||
likes: likesBoard,
|
||||
},
|
||||
highlights: {
|
||||
topPhoto,
|
||||
trendingEmotion,
|
||||
timeline,
|
||||
},
|
||||
feed,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeGuestNotification(raw: JsonValue): GuestNotificationSummary | null {
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return null;
|
||||
@@ -1818,6 +1983,37 @@ export async function getEventStats(slug: string): Promise<EventStats> {
|
||||
};
|
||||
}
|
||||
|
||||
export async function getEventEngagement(
|
||||
token: string,
|
||||
options: { locale?: string; guestName?: string; signal?: AbortSignal } = {}
|
||||
): Promise<EventEngagement> {
|
||||
const params = new URLSearchParams();
|
||||
if (options.guestName) {
|
||||
params.set('guest_name', options.guestName);
|
||||
}
|
||||
if (options.locale) {
|
||||
params.set('locale', options.locale);
|
||||
}
|
||||
|
||||
const query = params.toString();
|
||||
const response = await fetch(
|
||||
`/api/v1/events/${encodeURIComponent(token)}/achievements${query ? `?${query}` : ''}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(options.locale ? { 'X-Locale': options.locale } : {}),
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
cache: 'no-store',
|
||||
signal: options.signal,
|
||||
}
|
||||
);
|
||||
|
||||
const data = await jsonOrThrow<JsonValue>(response, 'Failed to load engagement data', { suppressToast: true });
|
||||
return normalizeEventEngagement(data);
|
||||
}
|
||||
|
||||
export async function getEventQrInvites(slug: string): Promise<EventQrInvite[]> {
|
||||
const response = await authorizedFetch(`${eventEndpoint(slug)}/join-tokens`);
|
||||
const payload = await jsonOrThrow<{ data: JsonValue[] }>(response, 'Failed to load invitations');
|
||||
|
||||
Reference in New Issue
Block a user