import { router } from 'expo-router'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ScrollView, StyleSheet, View, type NativeScrollEvent, type NativeSyntheticEvent } from 'react-native'; import { CategoryTabs, CommunityGrid, FeatureCarousel, Header, PageLayout, SectionHeader, StatusBarSpacer, type CommunityItem, type FeatureItem } from '@/components/bestai'; import type { FeatureItem as FeatureItemType } from '@/components/bestai/feature-carousel'; import { useAuth } from '@/hooks/use-auth'; import { useAuthGuard } from '@/hooks/use-auth-guard'; import { getActivities, type Activity } from '@/lib/api/activities'; import { categoriesWithChildren } from '@/lib/api/categories-with-children'; import type { CategoryTemplate, CategoryWithChildren } from '@/lib/types/template'; function coalesceText(...values: Array): string { for (const value of values) { const trimmed = (value ?? '').trim(); if (trimmed.length > 0) { return trimmed; } } return ''; } function translateTemplateToCommunity(template: CategoryTemplate): CommunityItem | null { const image = coalesceText(template.coverImageUrl, template.previewUrl); const title = coalesceText(template.titleEn, template.title); if (!image || !title) { return null; } const primaryTag = template.tags?.[0]; const chip = coalesceText(primaryTag?.nameEn, primaryTag?.name, template.aspectRatio) || 'Featured'; return { id: template.id, title, image, chip, actionLabel: 'Generate', }; } function translateActivity(activity: Activity): FeatureItem | null { const image = (activity.coverUrl || '').trim(); const title = (activity.titleEn || '').trim() || (activity.title || '').trim(); if (!image || !title) { return null; } const subtitle = (activity.descEn || '').trim() || (activity.desc || '').trim(); return { id: activity.id, title, subtitle, image, }; } export default function ExploreScreen() { const { isAuthenticated } = useAuth(); const { requireAuth } = useAuthGuard(); const [activeCategory, setActiveCategory] = useState(''); const [activityFeatures, setActivityFeatures] = useState([]); const [categoryCollection, setCategoryCollection] = useState([]); const scrollViewRef = useRef(null); const categoryPositionsRef = useRef>(new Map()); const isScrollingProgrammaticallyRef = useRef(false); useEffect(() => { const hydrateFeatureCarousel = async () => { try { const activities = await getActivities({ isActive: true }); const curatedFeatures = activities .map(translateActivity) .filter((feature): feature is FeatureItem => feature !== null); if (curatedFeatures.length > 0) { setActivityFeatures(curatedFeatures); } } catch (error) { const detail = error instanceof Error ? error.message : String(error); console.warn('FeatureCarousel activities feed unavailable:', detail); } }; hydrateFeatureCarousel(); }, []); useEffect(() => { let isActive = true; const hydrateCategories = async () => { try { const response = await categoriesWithChildren(); if (!isActive) { return; } const payload = response.success && Array.isArray(response.data) ? response.data : []; const curated = payload.filter((category) => category.id && (category.isActive ?? true)); setCategoryCollection(curated.length > 0 ? curated : payload); } catch (error) { if (isActive) { const detail = error instanceof Error ? error.message : String(error); console.warn('Categories feed unavailable:', detail); } } }; hydrateCategories(); return () => { isActive = false; }; }, [isAuthenticated]); useEffect(() => { if (categoryCollection.length === 0) { return; } const hasActive = categoryCollection.some((category) => category.id === activeCategory); if (!hasActive) { setActiveCategory(categoryCollection[0].id); } }, [categoryCollection, activeCategory]); const categoryOptions = useMemo(() => { if (categoryCollection.length === 0) { return []; } const options = categoryCollection .map((category) => { const label = coalesceText(category.nameEn, category.name); return label ? { id: category.id, label, } : null; }) .filter((category): category is { id: string; label: string } => category !== null); return options.length > 0 ? options : []; }, [categoryCollection]); const allCategoriesWithTemplates = useMemo(() => { return categoryCollection.map((category) => { const templates = (category.templates ?? []) .map(translateTemplateToCommunity) .filter((item): item is CommunityItem => item !== null); return { id: category.id, name: coalesceText(category.nameEn, category.name), templates, }; }); }, [categoryCollection]); const handleScroll = useCallback((event: NativeSyntheticEvent) => { if (isScrollingProgrammaticallyRef.current) { return; } const scrollY = event.nativeEvent.contentOffset.y; const positions = Array.from(categoryPositionsRef.current.entries()); let newActiveCategory = activeCategory; for (let i = positions.length - 1; i >= 0; i--) { const [categoryId, position] = positions[i]; if (scrollY >= position - 100) { newActiveCategory = categoryId; break; } } if (newActiveCategory !== activeCategory) { setActiveCategory(newActiveCategory); } }, [activeCategory]); const handleCategoryChange = useCallback((categoryId: string) => { const targetPosition = categoryPositionsRef.current.get(categoryId); if (targetPosition === undefined) { return; } isScrollingProgrammaticallyRef.current = true; scrollViewRef.current?.scrollTo({ y: targetPosition, animated: true, }); setTimeout(() => { isScrollingProgrammaticallyRef.current = false; }, 500); setActiveCategory(categoryId); }, []); const handleGeneratePress = useCallback((item: CommunityItem) => { requireAuth(() => { const templateId = item.id.split('-').pop() || item.id; router.push(`/templates/${templateId}`); }); }, [requireAuth]); const handleFeaturePress = useCallback((item: FeatureItemType) => { requireAuth(() => { // TODO: 实现功能逻辑 }); }, [requireAuth]); return (
{activityFeatures.length > 0 && ( )} {allCategoriesWithTemplates.map((category, index) => ( { const layout = event.nativeEvent.layout; categoryPositionsRef.current.set(category.id, layout.y); }} > ({ ...template, id: `${category.id}-${template.id || idx}`, }))} onPressAction={handleGeneratePress} /> ))} ); } const styles = StyleSheet.create({ scroll: { flex: 1, }, contentContainer: { paddingBottom: 16, }, });