upgrade to tamagui v2 and guest pwa overhaul
This commit is contained in:
6
resources/js/guest-v2/services/achievementsApi.ts
Normal file
6
resources/js/guest-v2/services/achievementsApi.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
fetchAchievements,
|
||||
type AchievementsPayload,
|
||||
type AchievementBadge,
|
||||
type LeaderboardEntry,
|
||||
} from '@/guest/services/achievementApi';
|
||||
95
resources/js/guest-v2/services/apiClient.ts
Normal file
95
resources/js/guest-v2/services/apiClient.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
export type ApiErrorPayload = {
|
||||
error?: {
|
||||
code?: string;
|
||||
title?: string;
|
||||
message?: string;
|
||||
meta?: Record<string, unknown>;
|
||||
};
|
||||
code?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export type ApiError = Error & {
|
||||
status?: number;
|
||||
code?: string;
|
||||
meta?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type FetchJsonResult<T> = {
|
||||
data: T | null;
|
||||
etag: string | null;
|
||||
notModified: boolean;
|
||||
status: number;
|
||||
};
|
||||
|
||||
type FetchJsonOptions = {
|
||||
method?: string;
|
||||
headers?: HeadersInit;
|
||||
body?: BodyInit | null;
|
||||
signal?: AbortSignal;
|
||||
etag?: string | null;
|
||||
noStore?: boolean;
|
||||
};
|
||||
|
||||
export async function fetchJson<T>(url: string, options: FetchJsonOptions = {}): Promise<FetchJsonResult<T>> {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'application/json',
|
||||
};
|
||||
|
||||
if (options.noStore) {
|
||||
headers['Cache-Control'] = 'no-store';
|
||||
}
|
||||
|
||||
if (options.etag) {
|
||||
headers['If-None-Match'] = options.etag;
|
||||
}
|
||||
|
||||
if (options.headers) {
|
||||
Object.assign(headers, options.headers as Record<string, string>);
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: options.method ?? 'GET',
|
||||
headers,
|
||||
body: options.body ?? null,
|
||||
signal: options.signal,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (response.status === 304) {
|
||||
return {
|
||||
data: null,
|
||||
etag: response.headers.get('ETag') ?? options.etag ?? null,
|
||||
notModified: true,
|
||||
status: response.status,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorPayload = await safeParseError(response);
|
||||
const error: ApiError = new Error(errorPayload?.error?.message ?? errorPayload?.message ?? `Request failed (${response.status})`);
|
||||
error.status = response.status;
|
||||
error.code = errorPayload?.error?.code ?? errorPayload?.code;
|
||||
if (errorPayload?.error?.meta) {
|
||||
error.meta = errorPayload.error.meta;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as T;
|
||||
return {
|
||||
data,
|
||||
etag: response.headers.get('ETag'),
|
||||
notModified: false,
|
||||
status: response.status,
|
||||
};
|
||||
}
|
||||
|
||||
async function safeParseError(response: Response): Promise<ApiErrorPayload | null> {
|
||||
try {
|
||||
const payload = (await response.clone().json()) as ApiErrorPayload;
|
||||
return payload;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
24
resources/js/guest-v2/services/emotionsApi.ts
Normal file
24
resources/js/guest-v2/services/emotionsApi.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { fetchJson } from './apiClient';
|
||||
import { getDeviceId } from '../lib/device';
|
||||
|
||||
export type EmotionItem = Record<string, unknown>;
|
||||
|
||||
type EmotionResponse = {
|
||||
data?: EmotionItem[];
|
||||
};
|
||||
|
||||
export async function fetchEmotions(eventToken: string, locale?: string) {
|
||||
const params = new URLSearchParams();
|
||||
if (locale) params.set('locale', locale);
|
||||
|
||||
const response = await fetchJson<EmotionResponse>(
|
||||
`/api/v1/events/${encodeURIComponent(eventToken)}/emotions${params.toString() ? `?${params.toString()}` : ''}`,
|
||||
{
|
||||
headers: {
|
||||
'X-Device-Id': getDeviceId(),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data?.data ?? [];
|
||||
}
|
||||
8
resources/js/guest-v2/services/eventApi.ts
Normal file
8
resources/js/guest-v2/services/eventApi.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export {
|
||||
fetchEvent,
|
||||
fetchStats,
|
||||
type EventData,
|
||||
type EventStats,
|
||||
FetchEventError,
|
||||
type FetchEventErrorCode,
|
||||
} from '@/guest/services/eventApi';
|
||||
16
resources/js/guest-v2/services/eventLink.ts
Normal file
16
resources/js/guest-v2/services/eventLink.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { EventData } from './eventApi';
|
||||
|
||||
export function buildEventShareLink(event: EventData | null, token: string | null): string {
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const origin = window.location.origin;
|
||||
const eventToken = token ?? event?.join_token ?? '';
|
||||
|
||||
if (!eventToken) {
|
||||
return origin;
|
||||
}
|
||||
|
||||
return `${origin}/e/${encodeURIComponent(eventToken)}`;
|
||||
}
|
||||
6
resources/js/guest-v2/services/galleryApi.ts
Normal file
6
resources/js/guest-v2/services/galleryApi.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
fetchGalleryMeta,
|
||||
fetchGalleryPhotos,
|
||||
type GalleryMetaResponse,
|
||||
type GalleryPhotoResource,
|
||||
} from '@/guest/services/galleryApi';
|
||||
6
resources/js/guest-v2/services/notificationsApi.ts
Normal file
6
resources/js/guest-v2/services/notificationsApi.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
fetchGuestNotifications,
|
||||
markGuestNotificationRead,
|
||||
dismissGuestNotification,
|
||||
type GuestNotificationItem,
|
||||
} from '@/guest/services/notificationApi';
|
||||
78
resources/js/guest-v2/services/photosApi.ts
Normal file
78
resources/js/guest-v2/services/photosApi.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { fetchJson } from './apiClient';
|
||||
import { getDeviceId } from '../lib/device';
|
||||
export { likePhoto, createPhotoShareLink, uploadPhoto } from '@/guest/services/photosApi';
|
||||
|
||||
export type GalleryPhoto = Record<string, unknown>;
|
||||
|
||||
type GalleryResponse = {
|
||||
data?: GalleryPhoto[];
|
||||
next_cursor?: string | null;
|
||||
latest_photo_at?: string | null;
|
||||
};
|
||||
|
||||
const galleryCache = new Map<string, { etag: string | null; data: GalleryResponse }>();
|
||||
|
||||
export async function fetchGallery(
|
||||
eventToken: string,
|
||||
params: { cursor?: string; since?: string; limit?: number; locale?: string } = {}
|
||||
) {
|
||||
const search = new URLSearchParams();
|
||||
if (params.cursor) search.set('cursor', params.cursor);
|
||||
if (params.since) search.set('since', params.since);
|
||||
if (params.limit) search.set('limit', params.limit.toString());
|
||||
if (params.locale) search.set('locale', params.locale);
|
||||
|
||||
const cacheKey = `${eventToken}:${search.toString()}`;
|
||||
const cached = galleryCache.get(cacheKey);
|
||||
|
||||
const response = await fetchJson<GalleryResponse>(
|
||||
`/api/v1/events/${encodeURIComponent(eventToken)}/photos${search.toString() ? `?${search.toString()}` : ''}`,
|
||||
{
|
||||
headers: {
|
||||
'X-Device-Id': getDeviceId(),
|
||||
},
|
||||
etag: cached?.etag ?? null,
|
||||
noStore: true,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.notModified && cached) {
|
||||
return { ...cached.data, notModified: true };
|
||||
}
|
||||
|
||||
const payload = response.data ?? { data: [], next_cursor: null, latest_photo_at: null };
|
||||
const items = Array.isArray((payload as GalleryResponse).data)
|
||||
? (payload as GalleryResponse).data ?? []
|
||||
: Array.isArray((payload as { photos?: GalleryPhoto[] }).photos)
|
||||
? (payload as { photos?: GalleryPhoto[] }).photos ?? []
|
||||
: Array.isArray(payload)
|
||||
? (payload as GalleryPhoto[])
|
||||
: [];
|
||||
|
||||
const data = {
|
||||
data: items,
|
||||
next_cursor: (payload as GalleryResponse).next_cursor ?? null,
|
||||
latest_photo_at: (payload as GalleryResponse).latest_photo_at ?? null,
|
||||
};
|
||||
|
||||
galleryCache.set(cacheKey, { etag: response.etag, data });
|
||||
|
||||
return { ...data, notModified: false };
|
||||
}
|
||||
|
||||
export async function fetchPhoto(photoId: number, locale?: string) {
|
||||
const search = locale ? `?locale=${encodeURIComponent(locale)}` : '';
|
||||
const response = await fetchJson<GalleryPhoto>(`/api/v1/photos/${photoId}${search}`, {
|
||||
headers: {
|
||||
'X-Device-Id': getDeviceId(),
|
||||
...(locale ? { 'X-Locale': locale } : {}),
|
||||
},
|
||||
noStore: true,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export function clearGalleryCache() {
|
||||
galleryCache.clear();
|
||||
}
|
||||
1
resources/js/guest-v2/services/pushApi.ts
Normal file
1
resources/js/guest-v2/services/pushApi.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { registerGuestPushSubscription, unregisterGuestPushSubscription } from '@/guest/services/pushApi';
|
||||
40
resources/js/guest-v2/services/statsApi.ts
Normal file
40
resources/js/guest-v2/services/statsApi.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { getDeviceId } from '../lib/device';
|
||||
import { fetchJson } from './apiClient';
|
||||
import type { EventStats } from './eventApi';
|
||||
|
||||
const statsCache = new Map<string, { etag: string | null; data: EventStats }>();
|
||||
|
||||
export async function fetchEventStats(eventToken: string): Promise<EventStats> {
|
||||
const cached = statsCache.get(eventToken);
|
||||
const response = await fetchJson<{ online_guests?: number; tasks_solved?: number; latest_photo_at?: string | null }>(
|
||||
`/api/v1/events/${encodeURIComponent(eventToken)}/stats`,
|
||||
{
|
||||
headers: {
|
||||
'X-Device-Id': getDeviceId(),
|
||||
},
|
||||
etag: cached?.etag ?? null,
|
||||
noStore: true,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.notModified && cached) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
const stats: EventStats = {
|
||||
onlineGuests: response.data?.online_guests ?? 0,
|
||||
tasksSolved: response.data?.tasks_solved ?? 0,
|
||||
latestPhotoAt: response.data?.latest_photo_at ?? null,
|
||||
};
|
||||
|
||||
statsCache.set(eventToken, { etag: response.etag, data: stats });
|
||||
return stats;
|
||||
}
|
||||
|
||||
export function clearStatsCache(eventToken?: string) {
|
||||
if (eventToken) {
|
||||
statsCache.delete(eventToken);
|
||||
return;
|
||||
}
|
||||
statsCache.clear();
|
||||
}
|
||||
26
resources/js/guest-v2/services/tasksApi.ts
Normal file
26
resources/js/guest-v2/services/tasksApi.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { fetchJson } from './apiClient';
|
||||
import { getDeviceId } from '../lib/device';
|
||||
|
||||
export type TaskItem = Record<string, unknown>;
|
||||
|
||||
type TaskResponse = {
|
||||
data?: TaskItem[];
|
||||
};
|
||||
|
||||
export async function fetchTasks(eventToken: string, options: { locale?: string; page?: number; perPage?: number } = {}) {
|
||||
const params = new URLSearchParams();
|
||||
if (options.locale) params.set('locale', options.locale);
|
||||
if (options.page) params.set('page', String(options.page));
|
||||
if (options.perPage) params.set('per_page', String(options.perPage));
|
||||
|
||||
const response = await fetchJson<TaskResponse>(
|
||||
`/api/v1/events/${encodeURIComponent(eventToken)}/tasks${params.toString() ? `?${params.toString()}` : ''}`,
|
||||
{
|
||||
headers: {
|
||||
'X-Device-Id': getDeviceId(),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data?.data ?? [];
|
||||
}
|
||||
3
resources/js/guest-v2/services/uploadApi.ts
Normal file
3
resources/js/guest-v2/services/uploadApi.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { uploadPhoto } from '@/guest/services/photosApi';
|
||||
export { enqueue } from '@/guest/queue/queue';
|
||||
export { useUploadQueue } from '@/guest/queue/hooks';
|
||||
Reference in New Issue
Block a user