Files
fotospiel-app/resources/js/guest/pages/HelpArticlePage.tsx
Codex Agent 03c7b20cae
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Improve guest help routing and loading
2026-01-14 09:00:12 +01:00

139 lines
5.1 KiB
TypeScript

import React from 'react';
import { Link, useParams } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { ArrowLeft, Loader2 } from 'lucide-react';
import { Page } from './_util';
import { useLocale } from '../i18n/LocaleContext';
import { useTranslation } from '../i18n/useTranslation';
import { getHelpArticle, type HelpArticleDetail } from '../services/helpApi';
import PullToRefresh from '../components/PullToRefresh';
export default function HelpArticlePage() {
const params = useParams<{ token?: string; slug: string }>();
const slug = params.slug;
const { locale } = useLocale();
const { t } = useTranslation();
const [article, setArticle] = React.useState<HelpArticleDetail | null>(null);
const [state, setState] = React.useState<'loading' | 'ready' | 'error'>('loading');
const basePath = params.token ? `/e/${encodeURIComponent(params.token)}/help` : '/help';
const loadArticle = React.useCallback(async () => {
if (!slug) {
setState('error');
return;
}
setState('loading');
try {
const result = await getHelpArticle(slug, locale);
setArticle(result.article);
setState('ready');
} catch (error) {
console.error('[HelpArticle] Failed to load article', error);
setState('error');
}
}, [slug, locale]);
React.useEffect(() => {
loadArticle();
}, [loadArticle]);
const title = state === 'loading'
? t('help.article.loadingTitle')
: (article?.title ?? t('help.article.unavailable'));
return (
<Page title={title}>
<PullToRefresh
onRefresh={loadArticle}
pullLabel={t('common.pullToRefresh')}
releaseLabel={t('common.releaseToRefresh')}
refreshingLabel={t('common.refreshing')}
>
<div className="mb-4">
<Button variant="outline" size="sm" className="rounded-full border-border/60 bg-background/70 px-3" asChild>
<Link to={basePath}>
<span className="inline-flex items-center gap-2">
<ArrowLeft className="h-4 w-4" aria-hidden />
{t('help.article.back')}
</span>
</Link>
</Button>
</div>
{state === 'loading' && (
<div className="rounded-2xl border border-border/60 bg-card/70 p-5">
<div className="flex items-center gap-3 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
<div>
<div className="font-medium text-foreground">{t('help.article.loadingTitle')}</div>
<div className="text-xs text-muted-foreground">{t('help.article.loadingDescription')}</div>
</div>
</div>
<div className="mt-4 space-y-2 animate-pulse">
<div className="h-3 w-2/3 rounded-full bg-muted/60" />
<div className="h-3 w-5/6 rounded-full bg-muted/60" />
<div className="h-3 w-1/2 rounded-full bg-muted/60" />
</div>
</div>
)}
{state === 'error' && (
<div className="space-y-3 rounded-lg border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive">
<p>{t('help.article.unavailable')}</p>
<Button variant="secondary" size="sm" onClick={loadArticle}>
{t('help.article.reload')}
</Button>
</div>
)}
{state === 'ready' && article && (
<article className="space-y-6">
<div className="space-y-2 text-sm text-muted-foreground">
{article.updated_at && (
<div>{t('help.article.updated', { date: formatDate(article.updated_at, locale) })}</div>
)}
</div>
<div className="overflow-x-auto">
<div
className="prose prose-sm max-w-none dark:prose-invert [&_table]:w-full [&_table]:text-sm [&_:where(p,ul,ol,li)]:text-foreground [&_:where(h1,h2,h3,h4,h5,h6)]:text-foreground"
dangerouslySetInnerHTML={{ __html: article.body_html ?? article.body_markdown ?? '' }}
/>
</div>
{article.related && article.related.length > 0 && (
<section className="space-y-3">
<h3 className="text-base font-semibold text-foreground">{t('help.article.relatedTitle')}</h3>
<div className="flex flex-wrap gap-2">
{article.related.map((rel) => (
<Button
key={rel.slug}
variant="outline"
size="sm"
asChild
>
<Link to={`${basePath}/${encodeURIComponent(rel.slug)}`}>
{rel.title ?? rel.slug}
</Link>
</Button>
))}
</div>
</section>
)}
</article>
)}
</PullToRefresh>
</Page>
);
}
function formatDate(value: string, locale: string): string {
try {
return new Date(value).toLocaleDateString(locale, {
day: '2-digit',
month: 'short',
year: 'numeric',
});
} catch {
return value;
}
}