import { useEffect, useMemo, useState } from 'react'; import { FlatList, Pressable, StyleSheet, View } from 'react-native'; import { Image } from 'expo-image'; import { ThemedText } from '@/components/themed-text'; import { ThemedView } from '@/components/themed-view'; import { ExternalLink } from '@/components/external-link'; import { Colors, Fonts } from '@/constants/theme'; import { useTranslation } from '@/localization/i18n'; import { useColorScheme } from '@/hooks/use-color-scheme'; const BASE_URL = 'https://simplico.net/wp-json/wp/v2'; const CATEGORY_SLUG = 'blogs'; const PER_PAGE = 10; type Post = { id: number; link: string; date: string; title: { rendered: string }; excerpt: { rendered: string }; _embedded?: { 'wp:featuredmedia'?: Array<{ source_url?: string; }>; }; }; type Category = { id: number; name: string; link: string; }; export default function BlogScreen() { const { t } = useTranslation(); const theme = useColorScheme() ?? 'light'; const palette = Colors[theme]; const [posts, setPosts] = useState([]); const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading'); const [categoryId, setCategoryId] = useState(null); const [categories, setCategories] = useState([]); const [page, setPage] = useState(1); const [loadingMore, setLoadingMore] = useState(false); const [totalPages, setTotalPages] = useState(null); const [loadMoreDebug, setLoadMoreDebug] = useState(''); const [langFilter, setLangFilter] = useState<'en' | 'th' | 'ja' | 'zh'>('en'); useEffect(() => { let isActive = true; async function loadPosts() { try { const fetchedCategoryId = await fetchCategoryId(CATEGORY_SLUG); if (!fetchedCategoryId) { setPosts([]); setCategories([]); setStatus('ready'); return; } setCategoryId(fetchedCategoryId); const categoryList = await fetchChildCategories(fetchedCategoryId); setCategories(categoryList); const response = await fetch( `${BASE_URL}/posts?per_page=${PER_PAGE}&page=1&_embed=1&cat=9&orderby=modified&order=desc` ); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = (await response.json()) as Post[]; if (!isActive) return; setPosts(data); setPage(1); setTotalPages(readTotalPages(response)); setStatus('ready'); } catch { if (isActive) setStatus('error'); } } loadPosts(); return () => { isActive = false; }; }, []); const filteredPosts = useMemo(() => { if (langFilter === 'en') { return posts.filter((post) => detectLangFromUrl(post.link) === 'en'); } return posts.filter((post) => detectLangFromUrl(post.link) === langFilter); }, [langFilter, posts]); const statusText = status === 'loading' ? t('blog.loading') : status === 'error' ? t('blog.error') : filteredPosts.length === 0 ? t('blog.empty') : ''; const canLoadMore = useMemo(() => { if (status !== 'ready') return false; if (!totalPages) return false; return page < totalPages; }, [page, status, totalPages]); async function handleLoadMore() { if (!categoryId || loadingMore || !canLoadMore) return; setLoadingMore(true); setLoadMoreDebug(''); try { const nextPage = page + 1; setLoadMoreDebug( `Requesting page ${nextPage}/${totalPages ?? '?'} (category ${categoryId})` ); const response = await fetch( `${BASE_URL}/posts?per_page=${PER_PAGE}&page=${nextPage}&_embed=1&cat=9&orderby=modified&order=desc` ); if (!response.ok) { const body = await response.json().catch(() => null); if (body?.code === 'rest_post_invalid_page_number') { setTotalPages(page); setLoadMoreDebug(`No more pages. Server says page ${nextPage} is invalid.`); return; } setLoadMoreDebug(`HTTP ${response.status}`); throw new Error(`HTTP ${response.status}`); } const data = (await response.json()) as Post[]; setPosts((prev) => mergeUniquePosts(prev, data)); setPage(nextPage); setTotalPages(readTotalPages(response)); setLoadMoreDebug( `Loaded ${data.length} posts. Total pages: ${readTotalPages(response) ?? '?'}` ); } finally { setLoadingMore(false); } } return ( String(item.id)} renderItem={({ item }) => ( {getFeaturedImage(item) ? ( ) : null} {stripTags(item.title.rendered)} {formatDate(item.date)} {truncateText(stripTags(item.excerpt.rendered), 160)} Read more )} ItemSeparatorComponent={() => } ListHeaderComponent={ {t('blog.title')} {statusText ? ( {statusText} ) : null} {categories.length > 0 ? ( Categories {categories.map((category) => ( {category.name} ))} ) : null} {t('blog.language')} {(['en', 'th', 'ja', 'zh'] as const).map((lang) => ( setLangFilter(lang)} style={[ styles.segmentItem, { borderColor: palette.border }, lang === langFilter ? { borderColor: palette.success, backgroundColor: palette.surface } : undefined, ]}> {labelForLang(t, lang)} ))} } ListFooterComponent={ loadingMore || loadMoreDebug ? ( {loadMoreDebug ? ( {loadMoreDebug} ) : null} {loadingMore ? {t('blog.loadingMore')} : null} ) : ( ) } onEndReached={() => { if (canLoadMore) { handleLoadMore(); } }} onEndReachedThreshold={0.6} extraData={langFilter} /> ); } function readTotalPages(response: Response) { const value = response.headers.get('x-wp-totalpages'); if (!value) return null; const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } function mergeUniquePosts(existing: Post[], incoming: Post[]) { const map = new Map(); for (const post of existing) map.set(post.id, post); for (const post of incoming) map.set(post.id, post); return Array.from(map.values()); } async function fetchChildCategories(parentId: number) { const response = await fetch(`${BASE_URL}/categories?parent=${parentId}&per_page=100`); if (!response.ok) return []; return (await response.json()) as Category[]; } async function fetchCategoryId(slug: string) { const response = await fetch(`${BASE_URL}/categories?slug=${encodeURIComponent(slug)}`); if (!response.ok) return null; const data = (await response.json()) as { id: number }[]; return data.length > 0 ? data[0].id : null; } function stripTags(html: string) { return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); } function truncateText(text: string, maxLength: number) { if (text.length <= maxLength) return text; return `${text.slice(0, maxLength - 1).trim()}…`; } function formatDate(value: string) { try { return new Date(value).toLocaleDateString(); } catch { return value; } } function getFeaturedImage(post: Post) { return post._embedded?.['wp:featuredmedia']?.[0]?.source_url ?? ''; } function labelForLang(t: (key: string) => string, lang: 'en' | 'th' | 'ja' | 'zh') { switch (lang) { case 'th': return t('blog.lang.th'); case 'ja': return t('blog.lang.ja'); case 'zh': return t('blog.lang.zh'); default: return t('blog.lang.en'); } } function detectLangFromUrl(url: string) { const lower = url.toLowerCase(); let path = lower; try { const parsed = new URL(lower); path = parsed.pathname; } catch { path = lower; } if (/(^|\/)th(\/|$)/.test(path) || /-th(\/|$)/.test(path)) return 'th'; if (/(^|\/)ja(\/|$)/.test(path) || /-ja(\/|$)/.test(path)) return 'ja'; if (/(^|\/)zh(\/|$)/.test(path) || /-zh(\/|$)/.test(path)) return 'zh'; return 'en'; } const styles = StyleSheet.create({ hero: { backgroundColor: '#E8E6DA', aspectRatio: 16 / 9, width: '100%', }, heroImage: { width: '100%', height: '100%', }, titleContainer: { flexDirection: 'row', gap: 8, paddingHorizontal: 16, paddingVertical: 12, }, section: { gap: 8, marginBottom: 16, paddingHorizontal: 16, }, card: { gap: 8, marginHorizontal: 16, padding: 12, borderRadius: 14, borderWidth: 1, borderColor: '#E1DACB', backgroundColor: '#FFFFFF', }, featuredImage: { height: 180, width: '100%', borderRadius: 12, }, meta: { opacity: 0.7, }, readMore: { paddingTop: 4, }, separator: { height: 12, }, footer: { paddingTop: 8, paddingBottom: 24, paddingHorizontal: 16, }, debugText: { fontSize: 12, opacity: 0.7, paddingBottom: 8, }, categoryRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 6, }, categoryBadge: { borderRadius: 9999, borderWidth: 1, borderColor: '#D9D1C2', backgroundColor: '#FFFFFF', paddingHorizontal: 10, paddingVertical: 4, }, categoryBadgeText: { fontSize: 12, fontWeight: '600', }, segmented: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, }, segmentItem: { borderRadius: 999, borderWidth: 1, borderColor: '#C6C6C6', paddingHorizontal: 12, paddingVertical: 6, }, segmentItemActive: { borderColor: '#1B8A5A', backgroundColor: '#E7F3EC', }, segmentText: { fontSize: 14, }, segmentTextActive: { fontWeight: '700', color: '#1B8A5A', }, });