Skip to content

Commit c021110

Browse files
committed
Fix UI lag and freezes by virtualizing product list and downsizing mock database
1 parent 6e2a40a commit c021110

2 files changed

Lines changed: 142 additions & 125 deletions

File tree

src/app/index.tsx

Lines changed: 139 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
SafeAreaView,
1414
Platform,
1515
useColorScheme,
16-
Linking,
1716
} from 'react-native';
1817
import { useRouter } from 'expo-router';
1918
import {
@@ -23,7 +22,6 @@ import {
2322
Search,
2423
Plus,
2524
X,
26-
Check,
2725
AlertCircle,
2826
TrendingDown,
2927
Layers,
@@ -94,6 +92,9 @@ export default function HomeScreen() {
9492
const [customBrand, setCustomBrand] = useState('');
9593
const [customCategory, setCustomCategory] = useState('ruj');
9694

95+
// Lazy loading state
96+
const [visibleLimit, setVisibleLimit] = useState(16);
97+
9798
// Load all simulated products
9899
const allProducts = useMemo(() => {
99100
return searchAndSimulateProducts('', 'all');
@@ -227,8 +228,6 @@ export default function HomeScreen() {
227228
const inWatchlist = isInWishlist(item.id);
228229
const discountPercent = Math.abs(Math.round(item.change));
229230
const isDiscounted = item.change < 0;
230-
231-
// Calculate original price roughly if discounted
232231
const originalPrice = cheapest ? (cheapest.price / (1 - discountPercent / 100)) : 0;
233232

234233
return (
@@ -237,7 +236,6 @@ export default function HomeScreen() {
237236
onPress={() => router.push({ pathname: '/product/[id]', params: { id: item.id } })}
238237
style={[styles.horizontalCard, { backgroundColor: themeColors.backgroundElement, borderColor: themeColors.border }]}
239238
>
240-
{/* Wishlist toggle badge */}
241239
<Pressable
242240
onPress={() => {
243241
if (inWatchlist) {
@@ -315,7 +313,6 @@ export default function HomeScreen() {
315313
onPress={() => router.push({ pathname: '/product/[id]', params: { id: item.id } })}
316314
style={[styles.gridCard, { backgroundColor: themeColors.backgroundElement, borderColor: themeColors.border }]}
317315
>
318-
{/* Wishlist button */}
319316
<Pressable
320317
onPress={() => {
321318
if (inWatchlist) {
@@ -449,132 +446,145 @@ export default function HomeScreen() {
449446
)}
450447
</View>
451448

452-
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: BottomTabInset + Spacing.six }}>
453-
454-
{/* Campaign Banner Carousel (Conditional) */}
455-
{!hasSearch && selectedCategory === 'all' && (
456-
<View style={styles.bannerContainer}>
457-
<ScrollView horizontal showsHorizontalScrollIndicator={false} pagingEnabled snapToAlignment="center" snapToInterval={width - Spacing.six} contentContainerStyle={styles.bannerScroll}>
458-
{CAMPAIGNS.map(item => (
459-
<View key={item.id} style={[styles.bannerCard, { backgroundColor: item.bg, borderColor: item.border }]}>
460-
<View style={styles.bannerBadge}>
461-
<Text style={styles.bannerBadgeText}>{item.badge}</Text>
462-
</View>
463-
<Text style={styles.bannerTitle} numberOfLines={1}>{item.title}</Text>
464-
<Text style={styles.bannerDesc} numberOfLines={2}>{item.desc}</Text>
465-
</View>
466-
))}
467-
</ScrollView>
468-
</View>
469-
)}
470-
471-
{/* Category Circle Row */}
472-
<View style={styles.categoriesContainer}>
473-
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.categoriesScroll}>
474-
{CATEGORIES.map(cat => {
475-
const isSelected = selectedCategory === cat.id;
476-
return (
477-
<Pressable
478-
key={cat.id}
479-
onPress={() => setSelectedCategory(cat.id)}
480-
style={styles.categoryCircleItem}
481-
>
482-
<View
483-
style={[
484-
styles.categoryCircle,
485-
{
486-
backgroundColor: isSelected ? themeColors.primary : themeColors.backgroundElement,
487-
borderColor: isSelected ? themeColors.accent : themeColors.border
488-
}
489-
]}
490-
>
491-
{getCategoryIcon(cat.id, isSelected ? '#4A3538' : themeColors.text, 20)}
492-
</View>
493-
<Text
494-
style={[
495-
styles.categoryLabel,
496-
{
497-
color: isSelected ? themeColors.text : themeColors.textSecondary,
498-
fontWeight: isSelected ? '700' : '500'
499-
}
500-
]}
501-
numberOfLines={1}
502-
>
503-
{cat.name}
504-
</Text>
505-
</Pressable>
506-
);
507-
})}
508-
</ScrollView>
509-
</View>
510-
511-
{/* MAIN PRODUCT DISPLAYS */}
512-
{hasSearch || selectedCategory !== 'all' ? (
513-
/* ACTIVE FILTER GRID DISPLAY */
514-
<View style={styles.sectionContainer}>
515-
<View style={styles.sectionHeaderRow}>
516-
<Text style={[styles.sectionTitle, { color: themeColors.text }]}>Arama ve Filtre Sonuçları</Text>
517-
<Text style={[styles.sectionSubtitleText, { color: themeColors.textSecondary }]}>
518-
{filteredProducts.length} ürün bulundu
519-
</Text>
520-
</View>
521-
<View style={styles.productGrid}>
522-
{filteredProducts.map(renderGridProductCard)}
523-
</View>
524-
{filteredProducts.length === 0 && (
525-
<View style={styles.emptyResults}>
526-
<AlertCircle size={32} color={themeColors.textSecondary} />
527-
<Text style={[styles.emptyResultsTitle, { color: themeColors.text }]}>Eşleşen Ürün Bulunamadı</Text>
528-
<Text style={[styles.emptyResultsSubtitle, { color: themeColors.textSecondary }]}>
529-
Seçtiğiniz filtreye uygun kozmetik ürünü bulunmamaktadır.
530-
</Text>
449+
{/* Virtualized Main FlatList Grid */}
450+
<FlatList
451+
data={hasSearch || selectedCategory !== 'all' ? filteredProducts.slice(0, visibleLimit) : allProducts.slice(0, visibleLimit)}
452+
keyExtractor={(item) => item.id}
453+
numColumns={2}
454+
columnWrapperStyle={styles.gridRow}
455+
contentContainerStyle={[styles.listContainer, { paddingBottom: BottomTabInset + Spacing.six }]}
456+
showsVerticalScrollIndicator={false}
457+
ListHeaderComponent={
458+
<View>
459+
{/* Campaign Banner Carousel */}
460+
{!hasSearch && selectedCategory === 'all' && (
461+
<View style={styles.bannerContainer}>
462+
<ScrollView horizontal showsHorizontalScrollIndicator={false} pagingEnabled snapToAlignment="center" snapToInterval={width - Spacing.six} contentContainerStyle={styles.bannerScroll}>
463+
{CAMPAIGNS.map(item => (
464+
<View key={item.id} style={[styles.bannerCard, { backgroundColor: item.bg, borderColor: item.border }]}>
465+
<View style={styles.bannerBadge}>
466+
<Text style={styles.bannerBadgeText}>{item.badge}</Text>
467+
</View>
468+
<Text style={styles.bannerTitle} numberOfLines={1}>{item.title}</Text>
469+
<Text style={styles.bannerDesc} numberOfLines={2}>{item.desc}</Text>
470+
</View>
471+
))}
472+
</ScrollView>
531473
</View>
532474
)}
533-
</View>
534-
) : (
535-
/* DEFAULT CURATED HOMEPAGE SECTIONS */
536-
<>
537-
{/* Günün Fırsatları */}
538-
<View style={styles.sectionContainer}>
539-
<View style={styles.sectionHeaderRow}>
540-
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
541-
<Percent size={16} color={themeColors.success} />
542-
<Text style={[styles.sectionTitle, { color: themeColors.text }]}>Günün İndirim Fırsatları</Text>
543-
</View>
544-
</View>
545-
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.horizontalScroll}>
546-
{dealsOfDay.map(renderHorizontalProductCard)}
475+
476+
{/* Category Circle Row */}
477+
<View style={styles.categoriesContainer}>
478+
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.categoriesScroll}>
479+
{CATEGORIES.map(cat => {
480+
const isSelected = selectedCategory === cat.id;
481+
return (
482+
<Pressable
483+
key={cat.id}
484+
onPress={() => setSelectedCategory(cat.id)}
485+
style={styles.categoryCircleItem}
486+
>
487+
<View
488+
style={[
489+
styles.categoryCircle,
490+
{
491+
backgroundColor: isSelected ? themeColors.primary : themeColors.backgroundElement,
492+
borderColor: isSelected ? themeColors.accent : themeColors.border
493+
}
494+
]}
495+
>
496+
{getCategoryIcon(cat.id, isSelected ? '#4A3538' : themeColors.text, 20)}
497+
</View>
498+
<Text
499+
style={[
500+
styles.categoryLabel,
501+
{
502+
color: isSelected ? themeColors.text : themeColors.textSecondary,
503+
fontWeight: isSelected ? '700' : '500'
504+
}
505+
]}
506+
numberOfLines={1}
507+
>
508+
{cat.name}
509+
</Text>
510+
</Pressable>
511+
);
512+
})}
547513
</ScrollView>
548514
</View>
549515

550-
{/* Popüler Ürünler */}
551-
<View style={styles.sectionContainer}>
552-
<View style={styles.sectionHeaderRow}>
553-
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
554-
<ShoppingCart size={16} color={themeColors.accent} />
555-
<Text style={[styles.sectionTitle, { color: themeColors.text }]}>Borsada En Çok Arananlar</Text>
516+
{/* Curated horizontal sections */}
517+
{!hasSearch && selectedCategory === 'all' ? (
518+
<>
519+
{/* Günün Fırsatları */}
520+
<View style={styles.sectionContainer}>
521+
<View style={styles.sectionHeaderRow}>
522+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
523+
<Percent size={16} color={themeColors.success} />
524+
<Text style={[styles.sectionTitle, { color: themeColors.text }]}>Günün İndirim Fırsatları</Text>
525+
</View>
526+
</View>
527+
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.horizontalScroll}>
528+
{dealsOfDay.map(renderHorizontalProductCard)}
529+
</ScrollView>
556530
</View>
557-
</View>
558-
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.horizontalScroll}>
559-
{popularProducts.map(renderHorizontalProductCard)}
560-
</ScrollView>
561-
</View>
562531

563-
{/* Sizin İçin Seçtiklerimiz (Tüm Ürünler Grid) */}
564-
<View style={styles.sectionContainer}>
565-
<View style={styles.sectionHeaderRow}>
566-
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
567-
<Sparkles size={16} color={themeColors.accent} />
568-
<Text style={[styles.sectionTitle, { color: themeColors.text }]}>Sizin İçin Seçtiklerimiz</Text>
532+
{/* Popüler Ürünler */}
533+
<View style={styles.sectionContainer}>
534+
<View style={styles.sectionHeaderRow}>
535+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
536+
<ShoppingCart size={16} color={themeColors.accent} />
537+
<Text style={[styles.sectionTitle, { color: themeColors.text }]}>Borsada En Çok Arananlar</Text>
538+
</View>
539+
</View>
540+
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.horizontalScroll}>
541+
{popularProducts.map(renderHorizontalProductCard)}
542+
</ScrollView>
543+
</View>
544+
545+
{/* Sizin İçin Seçtiklerimiz Section Title */}
546+
<View style={styles.sectionContainer}>
547+
<View style={styles.sectionHeaderRow}>
548+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
549+
<Sparkles size={16} color={themeColors.accent} />
550+
<Text style={[styles.sectionTitle, { color: themeColors.text }]}>Sizin İçin Seçtiklerimiz</Text>
551+
</View>
552+
</View>
553+
</View>
554+
</>
555+
) : (
556+
/* Arama / Filtre Sonuçları Section Title */
557+
<View style={styles.sectionContainer}>
558+
<View style={styles.sectionHeaderRow}>
559+
<Text style={[styles.sectionTitle, { color: themeColors.text }]}>Arama ve Filtre Sonuçları</Text>
560+
<Text style={[styles.sectionSubtitleText, { color: themeColors.textSecondary }]}>
561+
{filteredProducts.length} ürün bulundu
562+
</Text>
569563
</View>
570564
</View>
571-
<View style={styles.productGrid}>
572-
{allProducts.slice(0, 16).map(renderGridProductCard)}
573-
</View>
565+
)}
566+
</View>
567+
}
568+
renderItem={({ item }) => renderGridProductCard(item)}
569+
onEndReached={() => {
570+
const totalLength = hasSearch || selectedCategory !== 'all' ? filteredProducts.length : allProducts.length;
571+
if (visibleLimit < totalLength) {
572+
setVisibleLimit(prev => prev + 12);
573+
}
574+
}}
575+
onEndReachedThreshold={0.5}
576+
ListEmptyComponent={
577+
(hasSearch || selectedCategory !== 'all') && filteredProducts.length === 0 ? (
578+
<View style={styles.emptyResults}>
579+
<AlertCircle size={32} color={themeColors.textSecondary} />
580+
<Text style={[styles.emptyResultsTitle, { color: themeColors.text }]}>Eşleşen Ürün Bulunamadı</Text>
581+
<Text style={[styles.emptyResultsSubtitle, { color: themeColors.textSecondary }]}>
582+
Seçtiğiniz filtreye uygun kozmetik ürünü bulunmamaktadır.
583+
</Text>
574584
</View>
575-
</>
576-
)}
577-
</ScrollView>
585+
) : null
586+
}
587+
/>
578588

579589
{/* QUICK PRODUCT ADD MODAL */}
580590
<Modal
@@ -834,7 +844,10 @@ const styles = StyleSheet.create({
834844
borderWidth: 1,
835845
overflow: 'hidden',
836846
position: 'relative',
837-
marginBottom: 10,
847+
},
848+
gridRow: {
849+
justifyContent: 'space-between',
850+
marginBottom: 8,
838851
},
839852
cardHeartIcon: {
840853
position: 'absolute',
@@ -952,6 +965,9 @@ const styles = StyleSheet.create({
952965
fontSize: 11,
953966
textAlign: 'center',
954967
},
968+
listContainer: {
969+
paddingHorizontal: Spacing.three,
970+
},
955971
// Modal layout
956972
modalOverlay: {
957973
flex: 1,

src/constants/mockData.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -526,11 +526,12 @@ function generateMassiveProducts(): Product[] {
526526
list.push(p);
527527
});
528528

529-
brands.forEach((brand) => {
529+
// Limit generation to first 5 brands to keep catalog size around 350 items instead of 13,000+ items
530+
brands.slice(0, 5).forEach((brand) => {
530531
categories.forEach((cat) => {
531532
cat.prefix.forEach((prodName) => {
532533
const seedBase = hashCode(brand + prodName);
533-
const numVariations = 4 + (seedBase % 5); // 4-8 variations
534+
const numVariations = 1; // 1 variation per product shade instead of 4-8
534535

535536
for (let s = 0; s < numVariations; s++) {
536537
const shadeIndex = (hashCode(brand + prodName + s) % shades.length);

0 commit comments

Comments
 (0)