163 lines
4.4 KiB
TypeScript
163 lines
4.4 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') 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;
|
|
}
|
|
}
|