upgrade to tamagui v2 and guest pwa overhaul
This commit is contained in:
174
resources/js/guest-v2/screens/HelpCenterScreen.tsx
Normal file
174
resources/js/guest-v2/screens/HelpCenterScreen.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
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 { useAppearance } from '@/hooks/use-appearance';
|
||||
|
||||
export default function HelpCenterScreen() {
|
||||
const params = useParams<{ token?: string }>();
|
||||
const { locale } = useLocale();
|
||||
const { t } = useTranslation();
|
||||
const { resolved } = useAppearance();
|
||||
const isDark = resolved === 'dark';
|
||||
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 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>
|
||||
<Text fontSize="$5" fontWeight="$8">
|
||||
{t('help.center.title')}
|
||||
</Text>
|
||||
<Text fontSize="$2" color={mutedText} marginTop="$2">
|
||||
{t('help.center.subtitle')}
|
||||
</Text>
|
||||
</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>;
|
||||
}
|
||||
Reference in New Issue
Block a user