feat(tenant-admin): refresh event management experience
This commit is contained in:
@@ -2,12 +2,62 @@ import { authorizedFetch } from './auth/tokens';
|
||||
|
||||
type JsonValue = Record<string, any>;
|
||||
|
||||
export type TenantEvent = {
|
||||
id: number;
|
||||
name: string | Record<string, string>;
|
||||
slug: string;
|
||||
event_date: string | null;
|
||||
status: 'draft' | 'published' | 'archived';
|
||||
is_active?: boolean;
|
||||
description?: string | null;
|
||||
photo_count?: number;
|
||||
like_count?: number;
|
||||
};
|
||||
|
||||
export type TenantPhoto = {
|
||||
id: number;
|
||||
filename: string;
|
||||
original_name: string | null;
|
||||
mime_type: string | null;
|
||||
size: number;
|
||||
url: string;
|
||||
thumbnail_url: string;
|
||||
status: string;
|
||||
is_featured: boolean;
|
||||
likes_count: number;
|
||||
uploaded_at: string;
|
||||
uploader_name: string | null;
|
||||
};
|
||||
|
||||
export type EventStats = {
|
||||
total: number;
|
||||
featured: number;
|
||||
likes: number;
|
||||
recent_uploads: number;
|
||||
status: string;
|
||||
is_active: boolean;
|
||||
};
|
||||
|
||||
type EventListResponse = { data?: TenantEvent[] };
|
||||
type EventResponse = { data: TenantEvent };
|
||||
type CreatedEventResponse = { message: string; data: TenantEvent; balance: number };
|
||||
type PhotoResponse = { message: string; data: TenantPhoto };
|
||||
|
||||
type EventSavePayload = {
|
||||
name: string;
|
||||
slug: string;
|
||||
date?: string;
|
||||
status?: 'draft' | 'published' | 'archived';
|
||||
is_active?: boolean;
|
||||
};
|
||||
|
||||
async function jsonOrThrow<T>(response: Response, message: string): Promise<T> {
|
||||
if (!response.ok) {
|
||||
const body = await safeJson(response);
|
||||
console.error('[API]', message, response.status, body);
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
@@ -19,85 +69,112 @@ async function safeJson(response: Response): Promise<JsonValue | null> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getEvents(): Promise<any[]> {
|
||||
const response = await authorizedFetch('/api/v1/tenant/events');
|
||||
const data = await jsonOrThrow<{ data?: any[] }>(response, 'Failed to load events');
|
||||
return data.data ?? [];
|
||||
function normalizeEvent(event: TenantEvent): TenantEvent {
|
||||
return {
|
||||
...event,
|
||||
is_active: typeof event.is_active === 'boolean' ? event.is_active : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createEvent(payload: { name: string; slug: string; date?: string; is_active?: boolean }): Promise<number> {
|
||||
function normalizePhoto(photo: TenantPhoto): TenantPhoto {
|
||||
return {
|
||||
id: photo.id,
|
||||
filename: photo.filename,
|
||||
original_name: photo.original_name ?? null,
|
||||
mime_type: photo.mime_type ?? null,
|
||||
size: Number(photo.size ?? 0),
|
||||
url: photo.url,
|
||||
thumbnail_url: photo.thumbnail_url ?? photo.url,
|
||||
status: photo.status ?? 'approved',
|
||||
is_featured: Boolean(photo.is_featured),
|
||||
likes_count: Number(photo.likes_count ?? 0),
|
||||
uploaded_at: photo.uploaded_at,
|
||||
uploader_name: photo.uploader_name ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function eventEndpoint(slug: string): string {
|
||||
return `/api/v1/tenant/events/${encodeURIComponent(slug)}`;
|
||||
}
|
||||
|
||||
export async function getEvents(): Promise<TenantEvent[]> {
|
||||
const response = await authorizedFetch('/api/v1/tenant/events');
|
||||
const data = await jsonOrThrow<EventListResponse>(response, 'Failed to load events');
|
||||
return (data.data ?? []).map(normalizeEvent);
|
||||
}
|
||||
|
||||
export async function createEvent(payload: EventSavePayload): Promise<{ event: TenantEvent; balance: number }> {
|
||||
const response = await authorizedFetch('/api/v1/tenant/events', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await jsonOrThrow<{ id: number }>(response, 'Failed to create event');
|
||||
return data.id;
|
||||
const data = await jsonOrThrow<CreatedEventResponse>(response, 'Failed to create event');
|
||||
return { event: normalizeEvent(data.data), balance: data.balance };
|
||||
}
|
||||
|
||||
export async function updateEvent(
|
||||
id: number,
|
||||
payload: Partial<{ name: string; slug: string; date?: string; is_active?: boolean }>
|
||||
): Promise<void> {
|
||||
const response = await authorizedFetch(`/api/v1/tenant/events/${id}`, {
|
||||
export async function updateEvent(slug: string, payload: Partial<EventSavePayload>): Promise<TenantEvent> {
|
||||
const response = await authorizedFetch(eventEndpoint(slug), {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) {
|
||||
await safeJson(response);
|
||||
throw new Error('Failed to update event');
|
||||
}
|
||||
const data = await jsonOrThrow<EventResponse>(response, 'Failed to update event');
|
||||
return normalizeEvent(data.data);
|
||||
}
|
||||
|
||||
export async function getEventPhotos(id: number): Promise<any[]> {
|
||||
const response = await authorizedFetch(`/api/v1/tenant/events/${id}/photos`);
|
||||
const data = await jsonOrThrow<{ data?: any[] }>(response, 'Failed to load photos');
|
||||
return data.data ?? [];
|
||||
export async function getEvent(slug: string): Promise<TenantEvent> {
|
||||
const response = await authorizedFetch(eventEndpoint(slug));
|
||||
const data = await jsonOrThrow<EventResponse>(response, 'Failed to load event');
|
||||
return normalizeEvent(data.data);
|
||||
}
|
||||
|
||||
export async function featurePhoto(id: number): Promise<void> {
|
||||
const response = await authorizedFetch(`/api/v1/tenant/photos/${id}/feature`, { method: 'POST' });
|
||||
if (!response.ok) {
|
||||
await safeJson(response);
|
||||
throw new Error('Failed to feature photo');
|
||||
}
|
||||
export async function getEventPhotos(slug: string): Promise<TenantPhoto[]> {
|
||||
const response = await authorizedFetch(`${eventEndpoint(slug)}/photos`);
|
||||
const data = await jsonOrThrow<{ data?: TenantPhoto[] }>(response, 'Failed to load photos');
|
||||
return (data.data ?? []).map(normalizePhoto);
|
||||
}
|
||||
|
||||
export async function unfeaturePhoto(id: number): Promise<void> {
|
||||
const response = await authorizedFetch(`/api/v1/tenant/photos/${id}/unfeature`, { method: 'POST' });
|
||||
if (!response.ok) {
|
||||
await safeJson(response);
|
||||
throw new Error('Failed to unfeature photo');
|
||||
}
|
||||
export async function featurePhoto(slug: string, id: number): Promise<TenantPhoto> {
|
||||
const response = await authorizedFetch(`${eventEndpoint(slug)}/photos/${id}/feature`, { method: 'POST' });
|
||||
const data = await jsonOrThrow<PhotoResponse>(response, 'Failed to feature photo');
|
||||
return normalizePhoto(data.data);
|
||||
}
|
||||
|
||||
export async function getEvent(id: number): Promise<any> {
|
||||
const response = await authorizedFetch(`/api/v1/tenant/events/${id}`);
|
||||
return jsonOrThrow<any>(response, 'Failed to load event');
|
||||
export async function unfeaturePhoto(slug: string, id: number): Promise<TenantPhoto> {
|
||||
const response = await authorizedFetch(`${eventEndpoint(slug)}/photos/${id}/unfeature`, { method: 'POST' });
|
||||
const data = await jsonOrThrow<PhotoResponse>(response, 'Failed to unfeature photo');
|
||||
return normalizePhoto(data.data);
|
||||
}
|
||||
|
||||
export async function toggleEvent(id: number): Promise<boolean> {
|
||||
const response = await authorizedFetch(`/api/v1/tenant/events/${id}/toggle`, { method: 'POST' });
|
||||
const data = await jsonOrThrow<{ is_active: boolean }>(response, 'Failed to toggle event');
|
||||
return !!data.is_active;
|
||||
}
|
||||
|
||||
export async function getEventStats(id: number): Promise<{ total: number; featured: number; likes: number }> {
|
||||
const response = await authorizedFetch(`/api/v1/tenant/events/${id}/stats`);
|
||||
return jsonOrThrow<{ total: number; featured: number; likes: number }>(response, 'Failed to load stats');
|
||||
}
|
||||
|
||||
export async function createInviteLink(id: number): Promise<string> {
|
||||
const response = await authorizedFetch(`/api/v1/tenant/events/${id}/invites`, { method: 'POST' });
|
||||
const data = await jsonOrThrow<{ link: string }>(response, 'Failed to create invite');
|
||||
return data.link;
|
||||
}
|
||||
|
||||
export async function deletePhoto(id: number): Promise<void> {
|
||||
const response = await authorizedFetch(`/api/v1/tenant/photos/${id}`, { method: 'DELETE' });
|
||||
export async function deletePhoto(slug: string, id: number): Promise<void> {
|
||||
const response = await authorizedFetch(`${eventEndpoint(slug)}/photos/${id}`, { method: 'DELETE' });
|
||||
if (!response.ok) {
|
||||
await safeJson(response);
|
||||
throw new Error('Failed to delete photo');
|
||||
}
|
||||
}
|
||||
|
||||
export async function toggleEvent(slug: string): Promise<TenantEvent> {
|
||||
const response = await authorizedFetch(`${eventEndpoint(slug)}/toggle`, { method: 'POST' });
|
||||
const data = await jsonOrThrow<{ message: string; data: TenantEvent }>(response, 'Failed to toggle event');
|
||||
return normalizeEvent(data.data);
|
||||
}
|
||||
|
||||
export async function getEventStats(slug: string): Promise<EventStats> {
|
||||
const response = await authorizedFetch(`${eventEndpoint(slug)}/stats`);
|
||||
const data = await jsonOrThrow<EventStats>(response, 'Failed to load stats');
|
||||
return {
|
||||
total: Number(data.total ?? 0),
|
||||
featured: Number(data.featured ?? 0),
|
||||
likes: Number(data.likes ?? 0),
|
||||
recent_uploads: Number(data.recent_uploads ?? 0),
|
||||
status: data.status ?? 'draft',
|
||||
is_active: Boolean(data.is_active),
|
||||
};
|
||||
}
|
||||
|
||||
export async function createInviteLink(slug: string): Promise<{ link: string; token: string }> {
|
||||
const response = await authorizedFetch(`${eventEndpoint(slug)}/invites`, { method: 'POST' });
|
||||
return jsonOrThrow<{ link: string; token: string }>(response, 'Failed to create invite');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user