182 lines
6.9 KiB
TypeScript
182 lines
6.9 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 { Input } from '@tamagui/input';
|
|
import { Loader2, RefreshCcw, Search } 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 { getHelpArticles, type HelpArticleSummary } 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 HelpCenterScreen() {
|
|
const params = useParams<{ token?: string }>();
|
|
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 [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 [isOnline, setIsOnline] = React.useState(() => (typeof navigator !== 'undefined' ? navigator.onLine : true));
|
|
const basePath = params.token ? `/e/${encodeURIComponent(params.token)}/help` : '/help';
|
|
const brandName = 'Fotospiel';
|
|
const showStandaloneHeader = !params.token;
|
|
|
|
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]);
|
|
|
|
React.useEffect(() => {
|
|
if (typeof window === 'undefined') return;
|
|
const handleOnline = () => setIsOnline(true);
|
|
const handleOffline = () => setIsOnline(false);
|
|
window.addEventListener('online', handleOnline);
|
|
window.addEventListener('offline', handleOffline);
|
|
return () => {
|
|
window.removeEventListener('online', handleOnline);
|
|
window.removeEventListener('offline', handleOffline);
|
|
};
|
|
}, []);
|
|
|
|
const showOfflineBadge = servedFromCache && !isOnline;
|
|
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]);
|
|
|
|
const wrapper = (
|
|
<PullToRefresh
|
|
onRefresh={() => loadArticles(true)}
|
|
pullLabel={t('common.pullToRefresh')}
|
|
releaseLabel={t('common.releaseToRefresh')}
|
|
refreshingLabel={t('common.refreshing')}
|
|
>
|
|
<YStack gap="$4">
|
|
<SurfaceCard glow>
|
|
<XStack alignItems="center" gap="$3">
|
|
{showStandaloneHeader ? <EventLogo name={brandName} size="s" /> : null}
|
|
<YStack gap="$1">
|
|
<Text fontSize="$5" fontWeight="$8">
|
|
{t('help.center.title')}
|
|
</Text>
|
|
<Text fontSize="$2" color={mutedText}>
|
|
{t('help.center.subtitle')}
|
|
</Text>
|
|
</YStack>
|
|
</XStack>
|
|
</SurfaceCard>
|
|
|
|
<SurfaceCard>
|
|
<XStack gap="$2" alignItems="center">
|
|
<Search size={16} color={isDark ? '#F8FAFF' : '#0F172A'} />
|
|
<Input
|
|
flex={1}
|
|
placeholder={t('help.center.searchPlaceholder')}
|
|
value={query}
|
|
onChange={(event) => setQuery(event.currentTarget.value)}
|
|
aria-label={t('help.center.searchPlaceholder')}
|
|
/>
|
|
<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)'}
|
|
onPress={() => loadArticles(true)}
|
|
disabled={state === 'loading'}
|
|
>
|
|
{state === 'loading' ? <Loader2 size={16} className="animate-spin" /> : <RefreshCcw size={16} />}
|
|
</Button>
|
|
</XStack>
|
|
{showOfflineBadge ? (
|
|
<YStack marginTop="$3">
|
|
<Text fontSize="$2" color={mutedText}>
|
|
{t('help.center.offlineDescription')}
|
|
</Text>
|
|
</YStack>
|
|
) : null}
|
|
</SurfaceCard>
|
|
|
|
<YStack gap="$3">
|
|
<Text fontSize="$3" fontWeight="$7">
|
|
{t('help.center.listTitle')}
|
|
</Text>
|
|
{state === 'loading' ? (
|
|
<SurfaceCard>
|
|
<XStack alignItems="center" gap="$2">
|
|
<Loader2 size={16} className="animate-spin" />
|
|
<Text fontSize="$2" color={mutedText}>
|
|
{t('common.actions.loading')}
|
|
</Text>
|
|
</XStack>
|
|
</SurfaceCard>
|
|
) : null}
|
|
{state === 'error' ? (
|
|
<SurfaceCard>
|
|
<Text fontSize="$3" fontWeight="$7">
|
|
{t('help.center.error')}
|
|
</Text>
|
|
<Button size="$3" borderRadius="$pill" marginTop="$2" onPress={() => loadArticles(false)}>
|
|
{t('help.center.retry')}
|
|
</Button>
|
|
</SurfaceCard>
|
|
) : null}
|
|
{state === 'ready' && filteredArticles.length === 0 ? (
|
|
<SurfaceCard>
|
|
<Text fontSize="$2" color={mutedText}>
|
|
{t('help.center.empty')}
|
|
</Text>
|
|
</SurfaceCard>
|
|
) : null}
|
|
{state === 'ready' && filteredArticles.length > 0 ? (
|
|
<YStack gap="$3">
|
|
{filteredArticles.map((article) => (
|
|
<SurfaceCard key={article.slug} padding="$3">
|
|
<Link to={`${basePath}/${encodeURIComponent(article.slug)}`}>
|
|
<YStack gap="$2">
|
|
<Text fontSize="$4" fontWeight="$7">
|
|
{article.title}
|
|
</Text>
|
|
<Text fontSize="$2" color={mutedText}>
|
|
{article.summary}
|
|
</Text>
|
|
</YStack>
|
|
</Link>
|
|
</SurfaceCard>
|
|
))}
|
|
</YStack>
|
|
) : null}
|
|
</YStack>
|
|
</YStack>
|
|
</PullToRefresh>
|
|
);
|
|
|
|
if (params.token) {
|
|
return <AppShell>{wrapper}</AppShell>;
|
|
}
|
|
|
|
return <StandaloneShell>{wrapper}</StandaloneShell>;
|
|
}
|