refactor(guest): retire legacy guest app and move shared modules
This commit is contained in:
162
resources/js/shared/guest/services/helpApi.ts
Normal file
162
resources/js/shared/guest/services/helpApi.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
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') as Error & { status?: number };
|
||||
error.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: HelpArticleDetail | undefined = data?.data;
|
||||
const safeArticle: HelpArticleDetail = article ?? { slug, title: slug, summary: '' };
|
||||
writeCache(cacheKey, safeArticle);
|
||||
return { article: safeArticle, servedFromCache: false };
|
||||
} catch (error) {
|
||||
const cachedArticle: HelpArticleDetail | undefined = (cached as { data?: HelpArticleDetail } | null | undefined)?.data;
|
||||
if (cachedArticle) {
|
||||
return { article: cachedArticle, servedFromCache: true };
|
||||
}
|
||||
console.error('[HelpApi] Failed to fetch help article', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user