Created
June 25, 2026 19:16
-
-
Save shohan4556/f4ea963f1c798dccffba9ccc025633c2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useState, useCallback, useMemo } from 'react'; | |
| import { View, FlatList, StyleSheet, Text, Pressable } from 'react-native'; | |
| import { useNavigation } from '@react-navigation/native'; | |
| import useSWR from 'swr'; | |
| import { lightThemeColors } from '../../../shared/theme/colors'; | |
| import { CategoryTabs } from '../components/CategoryTabs'; | |
| import { LiveCard } from '../components/LiveCard'; | |
| import { LiveCardSkeleton } from '../components/LiveCardSkeleton'; | |
| import { getUserPosts, type FeedPost } from '../../../services/feed/feedService'; | |
| import { getCreatorCount } from '../../../services/profile/profileService'; | |
| import { useExploreFeed } from '../hooks/useExploreFeed'; | |
| import type { LiveCardData, Category } from '../types'; | |
| import type { RootStackNavigation } from '../../../app/navigation/types'; | |
| import { CreatorForm } from '../../profile/components/CreatorForm'; | |
| const CATEGORIES: Category[] = [ | |
| { id: 'explore', label: 'Explore' }, | |
| // { id: 'fashion', label: 'Fashion' }, | |
| { id: 'creators', label: 'Creators' }, | |
| ]; | |
| const PAGE_SIZE = 20; | |
| const SKELETON_COUNT = 6; | |
| const getImageHeight = (id: string): number => 160 + ((id.charCodeAt(0) || 0) % 80); | |
| const mapPostToCard = (post: FeedPost): LiveCardData => { | |
| const media = (post.media_urls as any[])?.[0]; | |
| const isVideo = media?.type === 'video'; | |
| return { | |
| id: post.post_id, | |
| imageHeight: getImageHeight(post.post_id), | |
| isLoading: false, | |
| isLive: false, | |
| isVideo, | |
| title: post.caption || '', | |
| imageUrl: media?.url ?? null, | |
| creatorName: post.author_name || 'Unknown', | |
| creatorAvatarLetter: (post.author_name?.[0] || '?').toUpperCase(), | |
| creatorAvatarUrl: post.author_avatar, | |
| viewerCount: 0, | |
| likeCount: post.like_count ?? 0, | |
| }; | |
| }; | |
| export const DiscoverScreen = () => { | |
| const navigation = useNavigation<RootStackNavigation>(); | |
| const [activeCategory, setActiveCategory] = useState('explore'); | |
| // ── SWR data hooks ──────────────────────────────────────────────── | |
| const { | |
| data: explorePages, | |
| error: exploreError, | |
| isLoading, | |
| isValidating, | |
| size, | |
| setSize, | |
| mutate: mutateExplore, | |
| } = useExploreFeed(); | |
| const { data: ownPosts = [] } = useSWR( | |
| 'own-posts-top', | |
| async () => { | |
| const { data } = await getUserPosts(undefined, 5); | |
| return data ?? []; | |
| }, | |
| { revalidateOnFocus: true }, | |
| ); | |
| const { data: creatorCount } = useSWR('creator-count', getCreatorCount); | |
| // ── Derived data ───────────────────────────────────────────────── | |
| const allExplorePosts = useMemo(() => (explorePages ?? []).flat(), [explorePages]); | |
| const postDataMap = useMemo(() => { | |
| const map = new Map<string, FeedPost>(); | |
| for (const post of allExplorePosts) map.set(post.post_id, post); | |
| for (const post of ownPosts) map.set(post.post_id, post); | |
| return map; | |
| }, [allExplorePosts, ownPosts]); | |
| const items = useMemo(() => { | |
| const ownCards = ownPosts.map(mapPostToCard); | |
| const exploreCards = allExplorePosts.map(mapPostToCard); | |
| return [...ownCards, ...exploreCards]; | |
| }, [ownPosts, allExplorePosts]); | |
| const isLoadingMore = isValidating && items.length > 0; | |
| const hasMore = !explorePages || explorePages[explorePages.length - 1]?.length >= PAGE_SIZE; | |
| // ── Creator state ───────────────────────────────────────────────── | |
| const [creatorFormVisible, setCreatorFormVisible] = useState(false); | |
| const handleRefresh = useCallback(() => { | |
| mutateExplore(); | |
| }, [mutateExplore]); | |
| const handleLoadMore = useCallback(() => { | |
| if (!isLoading && hasMore) setSize(size + 1); | |
| }, [isLoading, hasMore, size, setSize]); | |
| const handleCategorySelect = useCallback( | |
| (id: string) => { | |
| setActiveCategory(id); | |
| }, | |
| [], | |
| ); | |
| const handleCardPress = useCallback( | |
| (card: LiveCardData) => { | |
| const post = postDataMap.get(card.id); | |
| if (!post) return; | |
| navigation.navigate('PostDetail', { | |
| postId: post.post_id, | |
| caption: post.caption ?? '', | |
| mediaUrls: post.media_urls ?? [], | |
| tags: post.tags ?? [], | |
| likeCount: post.like_count ?? 0, | |
| commentCount: post.comment_count ?? 0, | |
| authorName: post.author_name ?? 'Unknown', | |
| authorAvatar: post.author_avatar ?? null, | |
| authorId: post.user_id ?? '', | |
| }); | |
| }, | |
| [navigation, postDataMap], | |
| ); | |
| const renderItem = useCallback( | |
| ({ item }: { item: LiveCardData }) => { | |
| if (item.isLoading) { | |
| return <LiveCardSkeleton imageHeight={item.imageHeight} />; | |
| } | |
| return <LiveCard data={item} onPress={() => handleCardPress(item)} />; | |
| }, | |
| [handleCardPress], | |
| ); | |
| const renderEmpty = useCallback(() => { | |
| return ( | |
| <View style={styles.empty}> | |
| <Text style={styles.emptyTitle}>No posts yet</Text> | |
| <Text style={styles.emptySubtitle}>Be the first to post something!</Text> | |
| </View> | |
| ); | |
| }, []); | |
| // Memoize skeleton cards so they aren't recreated every render | |
| const skeletonItems = useMemo( | |
| () => | |
| Array.from({ length: SKELETON_COUNT }, (_, i) => ({ | |
| id: `skel-${i}`, | |
| imageHeight: getImageHeight(`skel-${i}`), | |
| isLoading: true, | |
| isLive: false, | |
| isVideo: false, | |
| title: '', | |
| imageUrl: null, | |
| creatorName: '', | |
| creatorAvatarLetter: '?', | |
| creatorAvatarUrl: null, | |
| viewerCount: 0, | |
| likeCount: 0, | |
| } as LiveCardData)), | |
| [], | |
| ); | |
| /* | |
| const drawerGroups = [ | |
| { | |
| items: [{ label: 'Creator Center', onPress: () => {} }], | |
| backgroundColor: lightThemeColors.blue[100], | |
| }, | |
| { | |
| items: [ | |
| { label: 'Orders', onPress: () => {} }, | |
| { label: 'Cart', onPress: () => {} }, | |
| ], | |
| backgroundColor: lightThemeColors.green[100], | |
| }, | |
| { | |
| items: [{ label: 'Support', onPress: () => {} }], | |
| backgroundColor: lightThemeColors.red[100], | |
| }, | |
| { | |
| items: [{ label: 'Settings', onPress: () => {} }], | |
| backgroundColor: lightThemeColors.slate[100], | |
| }, | |
| ]; | |
| */ | |
| return ( | |
| <View style={styles.container}> | |
| <View style={styles.topRow}> | |
| {/* <Pressable onPress={() => setDrawerVisible(true)} hitSlop={10} style={styles.hamburger}> | |
| <Text style={styles.hamburgerText}>≡</Text> | |
| </Pressable> */} | |
| <CategoryTabs categories={CATEGORIES} activeId={activeCategory} onSelect={handleCategorySelect} /> | |
| </View> | |
| {/* <SideDrawer visible={drawerVisible} onClose={() => setDrawerVisible(false)} groups={drawerGroups} /> */} | |
| {activeCategory === 'creators' ? ( | |
| <View style={styles.creatorPanel}> | |
| {creatorCount != null && ( | |
| <> | |
| <View style={styles.creatorCountBadge}> | |
| <Text style={styles.creatorCountText}>{creatorCount}</Text> | |
| </View> | |
| <Text style={styles.creatorLabel}> | |
| {creatorCount === 1 ? 'creator' : 'creators'} registered! | |
| </Text> | |
| </> | |
| )} | |
| <Pressable style={styles.creatorJoinBtn} onPress={() => setCreatorFormVisible(true)}> | |
| <Text style={styles.creatorJoinBtnIcon}>+</Text> | |
| <View> | |
| <Text style={styles.creatorJoinBtnText}>Join as a creator</Text> | |
| <Text style={styles.creatorJoinBtnSub}>& earn cash</Text> | |
| </View> | |
| </Pressable> | |
| </View> | |
| ) : ( | |
| <> | |
| {exploreError && ( | |
| <View style={styles.errorBanner}> | |
| <Text style={styles.errorText}>{exploreError.message}</Text> | |
| <Text style={styles.retryText} onPress={() => mutateExplore()}> | |
| Retry | |
| </Text> | |
| </View> | |
| )} | |
| <FlatList | |
| data={isLoading ? skeletonItems : items} | |
| renderItem={renderItem} | |
| keyExtractor={item => item.id} | |
| numColumns={2} | |
| contentContainerStyle={styles.listContent} | |
| columnWrapperStyle={styles.columnWrapper} | |
| onEndReached={handleLoadMore} | |
| onEndReachedThreshold={0.5} | |
| refreshing={isValidating && items.length > 0} | |
| onRefresh={handleRefresh} | |
| showsVerticalScrollIndicator={false} | |
| ListEmptyComponent={renderEmpty} | |
| ListFooterComponent={ | |
| isLoadingMore ? <Text style={styles.loadingMore}>Loading more...</Text> : null | |
| } | |
| /> | |
| </> | |
| )} | |
| <CreatorForm visible={creatorFormVisible} onClose={() => { setCreatorFormVisible(false); }} /> | |
| </View> | |
| ); | |
| }; | |
| const styles = StyleSheet.create({ | |
| container: { | |
| flex: 1, | |
| backgroundColor: lightThemeColors.background.default, | |
| }, | |
| topRow: { | |
| flexDirection: 'row', | |
| alignItems: 'center', | |
| marginLeft: 20, | |
| }, | |
| hamburger: { | |
| paddingLeft: 16, | |
| paddingRight: 8, | |
| paddingVertical: 12, | |
| }, | |
| hamburgerText: { | |
| fontSize: 28, | |
| color: lightThemeColors.primary, | |
| lineHeight: 32, | |
| }, | |
| listContent: { | |
| paddingHorizontal: 16, | |
| paddingTop: 12, | |
| paddingBottom: 32, | |
| }, | |
| columnWrapper: { | |
| gap: 10, | |
| marginBottom: 10, | |
| }, | |
| empty: { | |
| flex: 1, | |
| paddingTop: 120, | |
| alignItems: 'center', | |
| }, | |
| emptyTitle: { | |
| fontFamily: 'Inter-Regular', | |
| fontSize: 16, | |
| fontWeight: '600', | |
| color: lightThemeColors.text.primary, | |
| marginBottom: 8, | |
| }, | |
| emptySubtitle: { | |
| fontFamily: 'Inter-Regular', | |
| fontSize: 14, | |
| color: lightThemeColors.text.secondary, | |
| }, | |
| errorBanner: { | |
| backgroundColor: lightThemeColors.red[50], | |
| borderWidth: 1, | |
| borderColor: lightThemeColors.red[200], | |
| marginHorizontal: 16, | |
| marginTop: 8, | |
| borderRadius: 8, | |
| padding: 10, | |
| flexDirection: 'row', | |
| justifyContent: 'space-between', | |
| alignItems: 'center', | |
| }, | |
| errorText: { | |
| fontFamily: 'Inter-Regular', | |
| fontSize: 13, | |
| color: lightThemeColors.error, | |
| flex: 1, | |
| }, | |
| retryText: { | |
| fontFamily: 'Inter-Regular', | |
| fontSize: 13, | |
| fontWeight: '600', | |
| color: lightThemeColors.primary, | |
| marginLeft: 12, | |
| }, | |
| loadingMore: { | |
| fontFamily: 'Inter-Regular', | |
| textAlign: 'center', | |
| paddingVertical: 16, | |
| fontSize: 13, | |
| color: lightThemeColors.text.disabled, | |
| }, | |
| creatorPanel: { | |
| flex: 1, | |
| alignItems: 'center', | |
| paddingTop: 60, | |
| paddingHorizontal: 32, | |
| }, | |
| creatorCountBadge: { | |
| width: 80, | |
| height: 80, | |
| borderRadius: 40, | |
| backgroundColor: lightThemeColors.blue[100], | |
| justifyContent: 'center', | |
| alignItems: 'center', | |
| marginBottom: 10, | |
| }, | |
| creatorCountText: { | |
| fontFamily: 'Inter-Bold', | |
| fontSize: 28, | |
| color: lightThemeColors.primary, | |
| }, | |
| creatorLabel: { | |
| fontFamily: 'Inter-Medium', | |
| fontSize: 16, | |
| color: lightThemeColors.text.secondary, | |
| marginBottom: 32, | |
| }, | |
| creatorJoinBtn: { | |
| flexDirection: 'row', | |
| alignItems: 'center', | |
| backgroundColor: lightThemeColors.primary, | |
| paddingHorizontal: 24, | |
| paddingVertical: 14, | |
| borderRadius: 16, | |
| gap: 12, | |
| }, | |
| creatorJoinBtnIcon: { | |
| fontFamily: 'Inter-Bold', | |
| fontSize: 22, | |
| color: '#fff', | |
| lineHeight: 26, | |
| }, | |
| creatorJoinBtnText: { | |
| fontFamily: 'Inter-SemiBold', | |
| fontSize: 14, | |
| color: '#fff', | |
| }, | |
| creatorJoinBtnSub: { | |
| fontFamily: 'Inter-Regular', | |
| fontSize: 12, | |
| color: 'rgba(255,255,255,0.7)', | |
| }, | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment