236 lines
6.7 KiB
TypeScript
236 lines
6.7 KiB
TypeScript
import { router } from 'expo-router';
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import { ScrollView, StyleSheet, View } 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 | null | undefined>): 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<string>();
|
|
const [activityFeatures, setActivityFeatures] = useState<FeatureItem[]>([]);
|
|
const [categoryCollection, setCategoryCollection] = useState<CategoryWithChildren[]>([]);
|
|
|
|
useEffect(() => {
|
|
const hydrateFeatureCarousel = async () => {
|
|
try {
|
|
const activities = await getActivities({ isActive: true });
|
|
console.log({ activities })
|
|
|
|
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();
|
|
|
|
return () => {
|
|
};
|
|
}, []);
|
|
|
|
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 activeCategoryRecord = useMemo(() => {
|
|
return categoryCollection.find((category) => category.id === activeCategory) ?? null;
|
|
}, [categoryCollection, activeCategory]);
|
|
|
|
const templateCommunityItems = useMemo(() => {
|
|
if (!activeCategoryRecord) {
|
|
return [];
|
|
}
|
|
|
|
return (activeCategoryRecord.templates ?? [])
|
|
.map(translateTemplateToCommunity)
|
|
.filter((item): item is CommunityItem => item !== null);
|
|
}, [activeCategoryRecord]);
|
|
|
|
const featureItems = useMemo(() => {
|
|
return activityFeatures.map((item, index) => ({
|
|
...item,
|
|
id: `${activeCategory}-${item.id || index}`,
|
|
}));
|
|
}, [activityFeatures]);
|
|
|
|
const communityItems = useMemo(() => {
|
|
const source: CommunityItem[] =
|
|
templateCommunityItems.length > 0 ? templateCommunityItems : [];
|
|
|
|
return source.map((item, index) => ({
|
|
...item,
|
|
id: `${activeCategory}-${item.id || index}`,
|
|
}));
|
|
}, [activeCategory, templateCommunityItems]);
|
|
|
|
const handleGeneratePress = (item: CommunityItem) => {
|
|
requireAuth(() => {
|
|
const templateId = item.id.split('-').pop() || item.id;
|
|
router.push(`/templates/${templateId}`);
|
|
});
|
|
};
|
|
|
|
const handleFeaturePress = (item: FeatureItemType) => {
|
|
requireAuth(() => {
|
|
console.log('用户已登录,使用功能:', item.title);
|
|
});
|
|
};
|
|
|
|
return (
|
|
<PageLayout backgroundColor="#050505">
|
|
<ScrollView
|
|
style={styles.scroll}
|
|
contentContainerStyle={styles.contentContainer}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
<StatusBarSpacer />
|
|
<Header />
|
|
<CategoryTabs categories={categoryOptions} activeId={activeCategory || ``} onChange={setActiveCategory} />
|
|
{featureItems.length > 0 && <FeatureCarousel items={featureItems} onPress={handleFeaturePress} />}
|
|
<SectionHeader title={activeCategoryRecord?.name || ``} />
|
|
<CommunityGrid items={communityItems} onPressAction={handleGeneratePress} />
|
|
<View style={styles.bottomSpacer} />
|
|
</ScrollView>
|
|
</PageLayout>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
scroll: {
|
|
flex: 1,
|
|
},
|
|
contentContainer: {
|
|
paddingTop: 16,
|
|
paddingBottom: 48,
|
|
},
|
|
bottomSpacer: {
|
|
height: 32,
|
|
},
|
|
});
|