Files
fotospiel-app/resources/js/guest/pages/HelpCenterPage.tsx

154 lines
5.8 KiB
TypeScript

import React from 'react';
import { Link, useParams } from 'react-router-dom';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Loader2, RefreshCcw } from 'lucide-react';
import { Page } from './_util';
import { useLocale } from '../i18n/LocaleContext';
import { useTranslation } from '../i18n/useTranslation';
import { getHelpArticles, type HelpArticleSummary } from '../services/helpApi';
export default function HelpCenterPage() {
const params = useParams<{ token?: string }>();
const { locale } = useLocale();
const { t } = useTranslation();
const [articles, setArticles] = React.useState<HelpArticleSummary[]>([]);
const [query, setQuery] = React.useState('');
const [state, setState] = React.useState<'idle' | 'loading' | 'ready' | 'error'>('loading');
const [servedFromCache, setServedFromCache] = React.useState(false);
const basePath = params.token ? `/e/${encodeURIComponent(params.token)}/help` : '/help';
const loadArticles = React.useCallback(async (forceRefresh = false) => {
setState('loading');
try {
const result = await getHelpArticles(locale, { forceRefresh });
setArticles(result.articles);
setServedFromCache(result.servedFromCache);
setState('ready');
} catch (error) {
console.error('[HelpCenter] Failed to load articles', error);
setState('error');
}
}, [locale]);
React.useEffect(() => {
loadArticles();
}, [loadArticles]);
const filteredArticles = React.useMemo(() => {
if (!query.trim()) {
return articles;
}
const needle = query.trim().toLowerCase();
return articles.filter((article) =>
`${article.title} ${article.summary}`.toLowerCase().includes(needle),
);
}, [articles, query]);
return (
<Page title={t('help.center.title')}>
<p className="mb-4 text-sm text-muted-foreground">{t('help.center.subtitle')}</p>
<div className="flex flex-col gap-2 rounded-xl border border-border/60 bg-background/50 p-3">
<div className="flex flex-col gap-3 sm:flex-row">
<Input
placeholder={t('help.center.searchPlaceholder')}
value={query}
onChange={(event) => setQuery(event.target.value)}
className="flex-1"
aria-label={t('help.center.searchPlaceholder')}
/>
<Button
variant="outline"
className="sm:w-auto"
onClick={() => loadArticles(true)}
disabled={state === 'loading'}
>
{state === 'loading' ? (
<span className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
{t('common.actions.loading')}
</span>
) : (
<span className="flex items-center gap-2">
<RefreshCcw className="h-4 w-4" />
{t('help.center.retry')}
</span>
)}
</Button>
</div>
{servedFromCache && (
<div className="flex items-center gap-2 rounded-lg bg-amber-50/70 px-3 py-2 text-xs text-amber-900 dark:bg-amber-400/10 dark:text-amber-200">
<Badge variant="secondary" className="bg-amber-200/80 text-amber-900 dark:bg-amber-500/40 dark:text-amber-100">
{t('help.center.offlineBadge')}
</Badge>
<span>{t('help.center.offlineDescription')}</span>
</div>
)}
</div>
<section className="mt-6 space-y-4">
<h2 className="text-base font-semibold text-foreground">{t('help.center.listTitle')}</h2>
{state === 'loading' && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
{t('common.actions.loading')}
</div>
)}
{state === 'error' && (
<div className="rounded-lg border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive">
<p>{t('help.center.error')}</p>
<Button
variant="secondary"
size="sm"
className="mt-3"
onClick={() => loadArticles(false)}
>
{t('help.center.retry')}
</Button>
</div>
)}
{state === 'ready' && filteredArticles.length === 0 && (
<div className="rounded-lg border border-dashed border-muted-foreground/30 p-4 text-sm text-muted-foreground">
{t('help.center.empty')}
</div>
)}
{state === 'ready' && filteredArticles.length > 0 && (
<div className="space-y-3">
{filteredArticles.map((article) => (
<Link
key={article.slug}
to={`${basePath}/${encodeURIComponent(article.slug)}`}
className="block rounded-2xl border border-border/60 bg-card/70 p-4 transition-colors hover:border-primary/60"
>
<div className="flex items-start justify-between gap-3">
<div>
<h3 className="text-base font-semibold text-foreground">{article.title}</h3>
<p className="mt-1 text-sm text-muted-foreground line-clamp-3">{article.summary}</p>
</div>
<span className="text-xs text-muted-foreground">
{article.updated_at ? formatDate(article.updated_at, locale) : ''}
</span>
</div>
</Link>
))}
</div>
)}
</section>
</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;
}
}