Files
fotospiel-app/resources/js/guest-v2/screens/HelpArticleScreen.tsx
Codex Agent 298a8375b6
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Update guest v2 branding and theming
2026-02-03 15:18:44 +01:00

178 lines
6.1 KiB
TypeScript

import React from 'react';
import { Link, useParams } from 'react-router-dom';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Button } from '@tamagui/button';
import { ArrowLeft, Loader2 } from 'lucide-react';
import AppShell from '../components/AppShell';
import StandaloneShell from '../components/StandaloneShell';
import SurfaceCard from '../components/SurfaceCard';
import PullToRefresh from '@/guest/components/PullToRefresh';
import { getHelpArticle, type HelpArticleDetail } from '@/guest/services/helpApi';
import { useLocale } from '@/guest/i18n/LocaleContext';
import { useTranslation } from '@/guest/i18n/useTranslation';
import EventLogo from '../components/EventLogo';
import { useGuestThemeVariant } from '../lib/guestTheme';
export default function HelpArticleScreen() {
const params = useParams<{ token?: string; slug: string }>();
const slug = params.slug;
const { locale } = useLocale();
const { t } = useTranslation();
const { isDark } = useGuestThemeVariant();
const mutedText = isDark ? 'rgba(248, 250, 252, 0.7)' : 'rgba(15, 23, 42, 0.65)';
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 brandName = 'Fotospiel';
const showStandaloneHeader = !params.token;
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'));
const wrapper = (
<PullToRefresh
onRefresh={loadArticle}
pullLabel={t('common.pullToRefresh')}
releaseLabel={t('common.releaseToRefresh')}
refreshingLabel={t('common.refreshing')}
>
<YStack gap="$4">
<SurfaceCard>
<Button
size="$3"
borderRadius="$pill"
backgroundColor={isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(15, 23, 42, 0.06)'}
borderWidth={1}
borderColor={isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(15, 23, 42, 0.12)'}
asChild
>
<Link to={basePath}>
<XStack alignItems="center" gap="$2">
<ArrowLeft size={16} />
<Text fontSize="$2" fontWeight="$7">
{t('help.article.back')}
</Text>
</XStack>
</Link>
</Button>
</SurfaceCard>
<SurfaceCard glow>
<XStack alignItems="center" gap="$3">
{showStandaloneHeader ? <EventLogo name={brandName} size="s" /> : null}
<YStack gap="$1">
<Text fontSize="$5" fontWeight="$8">
{title}
</Text>
{article?.updated_at ? (
<Text fontSize="$2" color={mutedText}>
{t('help.article.updated', { date: formatDate(article.updated_at, locale) })}
</Text>
) : null}
</YStack>
</XStack>
</SurfaceCard>
{state === 'loading' ? (
<SurfaceCard>
<XStack alignItems="center" gap="$2">
<Loader2 size={16} className="animate-spin" />
<Text fontSize="$2" color={mutedText}>
{t('help.article.loadingDescription')}
</Text>
</XStack>
</SurfaceCard>
) : null}
{state === 'error' ? (
<SurfaceCard>
<Text fontSize="$3" fontWeight="$7">
{t('help.article.unavailable')}
</Text>
<Button size="$3" borderRadius="$pill" marginTop="$2" onPress={loadArticle}>
{t('help.article.reload')}
</Button>
</SurfaceCard>
) : null}
{state === 'ready' && article ? (
<SurfaceCard>
<YStack gap="$4">
<YStack>
<div
style={{ color: mutedText, fontSize: '0.95rem', lineHeight: 1.6 }}
dangerouslySetInnerHTML={{ __html: article.body_html ?? article.body_markdown ?? '' }}
/>
</YStack>
{article.related && article.related.length > 0 ? (
<YStack gap="$2">
<Text fontSize="$3" fontWeight="$7">
{t('help.article.relatedTitle')}
</Text>
<XStack gap="$2" flexWrap="wrap">
{article.related.map((rel) => (
<Button
key={rel.slug}
size="$3"
borderRadius="$pill"
backgroundColor={isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(15, 23, 42, 0.06)'}
borderWidth={1}
borderColor={isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(15, 23, 42, 0.12)'}
asChild
>
<Link to={`${basePath}/${encodeURIComponent(rel.slug)}`}>
{rel.title ?? rel.slug}
</Link>
</Button>
))}
</XStack>
</YStack>
) : null}
</YStack>
</SurfaceCard>
) : null}
</YStack>
</PullToRefresh>
);
if (params.token) {
return <AppShell>{wrapper}</AppShell>;
}
return <StandaloneShell>{wrapper}</StandaloneShell>;
}
function formatDate(value: string, locale: string): string {
try {
return new Date(value).toLocaleDateString(locale, {
day: '2-digit',
month: 'short',
year: 'numeric',
});
} catch {
return value;
}
}