Files
fotospiel-app/resources/js/guest/services/helpApi.ts

161 lines
4.1 KiB
TypeScript

import type { LocaleCode } from '../i18n/messages';
export type HelpArticleSummary = {
slug: string;
title: string;
summary: string;
version_introduced?: string;
requires_app_version?: string | null;
status?: string;
translation_state?: string;
last_reviewed_at?: string;
owner?: string;
updated_at?: string;
related?: Array<{ slug: string; title?: string }>;
};
export type HelpArticleDetail = HelpArticleSummary & {
body_markdown?: string;
body_html?: string;
source_path?: string;
};
export interface HelpListResult {
articles: HelpArticleSummary[];
servedFromCache: boolean;
}
export interface HelpArticleResult {
article: HelpArticleDetail;
servedFromCache: boolean;
}
const AUDIENCE = 'guest';
const LIST_CACHE_TTL = 1000 * 60 * 60 * 6; // 6 hours
const DETAIL_CACHE_TTL = 1000 * 60 * 60 * 24; // 24 hours
interface CacheRecord<T> {
storedAt: number;
data: T;
}
function listCacheKey(locale: LocaleCode): string {
return `help:list:${AUDIENCE}:${locale}`;
}
function detailCacheKey(locale: LocaleCode, slug: string): string {
return `help:article:${AUDIENCE}:${locale}:${slug}`;
}
function readCache<T>(key: string, ttl: number): CacheRecord<T> | null {
if (typeof window === 'undefined') {
return null;
}
try {
const raw = window.localStorage.getItem(key);
if (!raw) {
return null;
}
const parsed = JSON.parse(raw) as CacheRecord<T>;
if (!parsed?.data || !parsed?.storedAt) {
return null;
}
if (Date.now() - parsed.storedAt > ttl) {
return null;
}
return parsed;
} catch (error) {
console.warn('[HelpApi] Failed to read cache', error);
return null;
}
}
function writeCache<T>(key: string, data: T): void {
if (typeof window === 'undefined') {
return;
}
try {
const payload: CacheRecord<T> = {
storedAt: Date.now(),
data,
};
window.localStorage.setItem(key, JSON.stringify(payload));
} catch (error) {
console.warn('[HelpApi] Failed to write cache', error);
}
}
async function requestJson<T>(url: string): Promise<T> {
const response = await fetch(url, {
headers: {
'Accept': 'application/json',
},
credentials: 'same-origin',
});
if (!response.ok) {
const error = new Error('Help request failed');
(error as any).status = response.status;
throw error;
}
const payload = await response.json();
return payload as T;
}
export async function getHelpArticles(locale: LocaleCode, options?: { forceRefresh?: boolean }): Promise<HelpListResult> {
const cacheKey = listCacheKey(locale);
const cached = readCache<HelpArticleSummary[]>(cacheKey, LIST_CACHE_TTL);
if (cached && !options?.forceRefresh) {
return { articles: cached.data, servedFromCache: true };
}
try {
const params = new URLSearchParams({
audience: AUDIENCE,
locale,
});
const data = await requestJson<{ data?: HelpArticleSummary[] }>(`/api/v1/help?${params.toString()}`);
const articles = Array.isArray(data?.data) ? data.data : [];
writeCache(cacheKey, articles);
return { articles, servedFromCache: false };
} catch (error) {
if (cached) {
return { articles: cached.data, servedFromCache: true };
}
console.error('[HelpApi] Failed to fetch help articles', error);
throw error;
}
}
export async function getHelpArticle(slug: string, locale: LocaleCode): Promise<HelpArticleResult> {
const cacheKey = detailCacheKey(locale, slug);
const cached = readCache<HelpArticleDetail>(cacheKey, DETAIL_CACHE_TTL);
if (cached) {
return { article: cached.data, servedFromCache: true };
}
try {
const params = new URLSearchParams({
audience: AUDIENCE,
locale,
});
const data = await requestJson<{ data?: HelpArticleDetail }>(`/api/v1/help/${encodeURIComponent(slug)}?${params.toString()}`);
const article = data?.data ?? { slug, title: slug, summary: '' };
writeCache(cacheKey, article);
return { article, servedFromCache: false };
} catch (error) {
if (cached) {
return { article: cached.data, servedFromCache: true };
}
console.error('[HelpApi] Failed to fetch help article', error);
throw error;
}
}