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