Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save shohan4556/f4ea963f1c798dccffba9ccc025633c2 to your computer and use it in GitHub Desktop.

Select an option

Save shohan4556/f4ea963f1c798dccffba9ccc025633c2 to your computer and use it in GitHub Desktop.
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