/ app / (tabs) / index.tsx
index.tsx
  1  import React, { useState, useEffect, useCallback } from 'react';
  2  import {
  3    StyleSheet,
  4    View,
  5    Text,
  6    TouchableOpacity,
  7    FlatList,
  8    ScrollView,
  9    Image,
 10    Dimensions,
 11  } from 'react-native';
 12  import { useRouter } from 'expo-router';
 13  import { useFocusEffect } from '@react-navigation/native';
 14  import { useTheme } from '@/constants/ThemeContext';
 15  import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
 16  import { Colors } from '@/constants/Colors';
 17  import { LinearGradient } from 'expo-linear-gradient';
 18  import { MANGA_API_URL } from '@/constants/Config';
 19  import MangaCard from '@/components/MangaCard';
 20  import {
 21    RecentlyReadSkeleton,
 22    TrendingSkeleton,
 23    NewReleasesSkeleton,
 24    FeaturedMangaSkeleton,
 25  } from '@/components/SkeletonLoading';
 26  import { SmoothRefreshControl } from '@/components/SmoothRefreshControl';
 27  import { PageTransition } from '@/components/PageTransition';
 28  import {
 29    parseMostViewedManga,
 30    parseNewReleases,
 31  } from '@/services/mangaFireService';
 32  import { useSafeAreaInsets } from 'react-native-safe-area-context';
 33  import { useCloudflareDetection } from '@/hooks/useCloudflareDetection';
 34  import axios from 'axios';
 35  import { MangaItem, RecentMangaItem } from '@/types';
 36  import { getRecentlyReadManga } from '@/services/readChapterService';
 37  
 38  const { width: SCREEN_WIDTH } = Dimensions.get('window');
 39  
 40  const TRENDING_CARD_WIDTH = 200;
 41  const TRENDING_CARD_HEIGHT = 260;
 42  const FEATURED_HEIGHT = 280;
 43  const RECENTLY_READ_CARD_WIDTH = Math.min(160, (SCREEN_WIDTH - 64) / 2);
 44  
 45  const DEFAULT_MANGA_COVER =
 46    'https://static.mangafire.to/default/img/no-image.jpg';
 47  
 48  export default function HomeScreen() {
 49    const router = useRouter();
 50    const { actualTheme, accentColor } = useTheme();
 51    const colors = Colors[actualTheme];
 52    const themeColors = { ...colors, primary: accentColor || colors.primary };
 53    const { checkForCloudflare, resetCloudflareDetection } =
 54      useCloudflareDetection();
 55    const insets = useSafeAreaInsets();
 56  
 57    const [mostViewedManga, setMostViewedManga] = useState<MangaItem[]>([]);
 58    const [newReleases, setNewReleases] = useState<MangaItem[]>([]);
 59    const [isLoading, setIsLoading] = useState<boolean>(true);
 60    const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
 61    const [error, setError] = useState<string | null>(null);
 62    const [featuredManga, setFeaturedManga] = useState<MangaItem | null>(null);
 63  
 64    const [recentlyReadManga, setRecentlyReadManga] = useState<RecentMangaItem[]>(
 65      []
 66    );
 67    const [isRecentMangaLoading, setIsRecentMangaLoading] =
 68      useState<boolean>(true);
 69  
 70    const fetchMangaData = useCallback(async () => {
 71      try {
 72        setError(null);
 73        if (!isRefreshing) {
 74          setIsLoading(true);
 75        }
 76  
 77        const response = await axios.get(`${MANGA_API_URL}/home`, {
 78          headers: {
 79            'User-Agent':
 80              'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1',
 81          },
 82          timeout: 10000,
 83        });
 84  
 85        const html = response.data as string;
 86  
 87        if (checkForCloudflare(html)) {
 88          return;
 89        }
 90  
 91        const parsedMostViewed = parseMostViewedManga(html);
 92        const parsedNewReleases = parseNewReleases(html);
 93  
 94        setMostViewedManga(parsedMostViewed);
 95        setNewReleases(parsedNewReleases);
 96  
 97        if (parsedMostViewed.length > 0) {
 98          setFeaturedManga(parsedMostViewed[0] || null);
 99        }
100      } catch (error) {
101        console.error('Error fetching manga data:', error);
102        setError(
103          'An error occurred while fetching manga data. Please try again.'
104        );
105      } finally {
106        setIsLoading(false);
107        setIsRefreshing(false);
108      }
109    }, [isRefreshing, checkForCloudflare]);
110  
111    const fetchRecentlyReadManga = useCallback(async () => {
112      try {
113        setIsRecentMangaLoading(true);
114        const recentManga = await getRecentlyReadManga(6);
115  
116        const processedManga = recentManga.map((manga) => ({
117          ...manga,
118          bannerImage: manga.bannerImage || DEFAULT_MANGA_COVER,
119        }));
120  
121        setRecentlyReadManga(processedManga);
122      } catch (error) {
123        console.error('Error fetching recently read manga:', error);
124      } finally {
125        setIsRecentMangaLoading(false);
126      }
127    }, []);
128  
129    useEffect(() => {
130      fetchMangaData();
131      fetchRecentlyReadManga();
132      return () => {
133        resetCloudflareDetection();
134      };
135    }, []);
136  
137    useFocusEffect(
138      useCallback(() => {
139        fetchRecentlyReadManga();
140      }, [fetchRecentlyReadManga])
141    );
142  
143    const handleRefresh = useCallback(() => {
144      setIsRefreshing(true);
145      fetchMangaData();
146      fetchRecentlyReadManga();
147    }, [fetchMangaData, fetchRecentlyReadManga]);
148  
149    const renderSectionTitle = useCallback(
150      (title: string, iconName: keyof typeof Ionicons.glyphMap) => (
151        <View style={styles.sectionTitleContainer}>
152          <View
153            style={[
154              styles.iconBackground,
155              { backgroundColor: themeColors.primary + '20' },
156            ]}
157          >
158            <Ionicons
159              name={iconName}
160              size={20}
161              color={themeColors.primary}
162              accessibilityElementsHidden={true}
163            />
164          </View>
165          <Text
166            style={[styles.sectionTitle, { color: themeColors.text }]}
167            accessibilityRole="header"
168          >
169            {title}
170          </Text>
171        </View>
172      ),
173      [themeColors]
174    );
175  
176    const renderTrendingItem = useCallback(
177      ({ item, index }: { item: MangaItem; index: number }) => (
178        <TouchableOpacity
179          style={[styles.trendingItem, { marginLeft: index === 0 ? 16 : 12 }]}
180          onPress={() => router.navigate(`/manga/${item.id}`)}
181          activeOpacity={0.7}
182          accessibilityRole="button"
183          accessibilityLabel={`View ${item.title}`}
184          accessibilityHint={
185            item.rank
186              ? `Ranked #${item.rank} in trending`
187              : 'Currently trending manga'
188          }
189        >
190          <Image
191            source={{ uri: item.imageUrl }}
192            style={styles.trendingImage}
193            accessibilityLabel={`Cover image for ${item.title}`}
194          />
195          <LinearGradient
196            colors={['transparent', 'rgba(0,0,0,0.9)']}
197            style={styles.trendingGradient}
198          >
199            <View style={styles.trendingContent}>
200              <Text style={styles.trendingTitle} numberOfLines={2}>
201                {item.title}
202              </Text>
203              {item.rank && (
204                <View
205                  style={[
206                    styles.rankBadge,
207                    { backgroundColor: themeColors.primary },
208                  ]}
209                >
210                  <Text style={styles.rankText}>#{item.rank}</Text>
211                </View>
212              )}
213            </View>
214          </LinearGradient>
215        </TouchableOpacity>
216      ),
217      [router, themeColors.primary]
218    );
219  
220    const renderRecentlyReadItem = useCallback(
221      ({ item, index }: { item: RecentMangaItem; index: number }) => {
222        const lastReadChapter = item.lastReadChapter
223          ? `Chapter ${item.lastReadChapter}`
224          : 'Not started';
225  
226        return (
227          <View
228            style={[
229              styles.recentlyReadItem,
230              { marginLeft: index === 0 ? 16 : 12 },
231            ]}
232          >
233            <MangaCard
234              title={item.title}
235              imageUrl={item.bannerImage}
236              onPress={() => router.navigate(`/manga/${item.id}`)}
237              lastReadChapter={lastReadChapter}
238              style={styles.recentlyReadCard}
239              context="manga"
240              mangaId={item.id}
241            />
242            <Text
243              style={[styles.recentlyReadTitle, { color: themeColors.text }]}
244              numberOfLines={2}
245            >
246              {item.title}
247            </Text>
248          </View>
249        );
250      },
251      [router, themeColors.text]
252    );
253  
254    const renderNewReleaseGrid = useCallback(() => {
255      return (
256        <View style={styles.newReleaseGrid}>
257          {newReleases.map((item) => (
258            <View key={item.id} style={styles.newReleaseWrapper}>
259              <TouchableOpacity
260                onPress={() => router.navigate(`/manga/${item.id}`)}
261                activeOpacity={0.7}
262                style={styles.newReleaseCard}
263              >
264                <MangaCard
265                  title={item.title}
266                  imageUrl={item.imageUrl}
267                  onPress={() => router.navigate(`/manga/${item.id}`)}
268                  lastReadChapter={null}
269                  style={styles.card}
270                  context="manga"
271                  mangaId={item.id}
272                />
273                <View style={styles.titleContainer}>
274                  <Text
275                    style={[styles.mangaTitle, { color: themeColors.text }]}
276                    numberOfLines={2}
277                  >
278                    {item.title}
279                  </Text>
280                </View>
281              </TouchableOpacity>
282            </View>
283          ))}
284        </View>
285      );
286    }, [newReleases, router, themeColors.text]);
287  
288    const renderContinueReadingSection = useCallback(() => {
289      if (isRecentMangaLoading) {
290        return <RecentlyReadSkeleton />;
291      }
292  
293      if (recentlyReadManga.length === 0) {
294        return (
295          <View
296            style={[
297              styles.emptyStateContainer,
298              { backgroundColor: themeColors.card + '50' },
299            ]}
300          >
301            <Ionicons
302              name="book-outline"
303              size={40}
304              color={themeColors.text + '70'}
305            />
306            <Text
307              style={[styles.emptyStateText, { color: themeColors.text + '90' }]}
308            >
309              Manga you&apos;re reading will appear here
310            </Text>
311            <TouchableOpacity
312              style={[
313                styles.browseButton,
314                { backgroundColor: themeColors.primary + '20' },
315              ]}
316              onPress={() => router.navigate('/mangasearch')}
317            >
318              <Text
319                style={[styles.browseButtonText, { color: themeColors.primary }]}
320              >
321                Browse Manga
322              </Text>
323            </TouchableOpacity>
324          </View>
325        );
326      }
327  
328      return (
329        <FlatList
330          data={recentlyReadManga}
331          renderItem={renderRecentlyReadItem}
332          keyExtractor={(item) => item.id}
333          horizontal
334          showsHorizontalScrollIndicator={false}
335          contentContainerStyle={styles.recentlyReadList}
336          decelerationRate="fast"
337          getItemLayout={(_data, index) => ({
338            length: RECENTLY_READ_CARD_WIDTH + 12,
339            offset: (RECENTLY_READ_CARD_WIDTH + 12) * index,
340            index,
341          })}
342          removeClippedSubviews={true}
343          maxToRenderPerBatch={3}
344          updateCellsBatchingPeriod={100}
345          windowSize={8}
346        />
347      );
348    }, [
349      isRecentMangaLoading,
350      recentlyReadManga,
351      themeColors,
352      router,
353      renderRecentlyReadItem,
354    ]);
355  
356    const renderFeaturedManga = useCallback(() => {
357      if (!featuredManga) return null;
358  
359      return (
360        <TouchableOpacity
361          style={[styles.featuredContainer, { marginTop: insets.top + 16 }]}
362          onPress={() => router.navigate(`/manga/${featuredManga.id}`)}
363          activeOpacity={0.8}
364        >
365          <Image
366            source={{ uri: featuredManga.imageUrl }}
367            style={styles.featuredImage}
368          />
369          <LinearGradient
370            colors={['transparent', 'rgba(0,0,0,0.8)']}
371            style={styles.featuredGradient}
372          >
373            <View style={styles.featuredContent}>
374              <View style={styles.featuredBadge}>
375                <MaterialCommunityIcons name="fire" size={16} color="#FFF" />
376                <Text style={styles.featuredBadgeText}>Featured</Text>
377              </View>
378              <Text style={styles.featuredTitle} numberOfLines={2}>
379                {featuredManga.title}
380              </Text>
381              <TouchableOpacity
382                style={[
383                  styles.readNowButton,
384                  { backgroundColor: themeColors.primary },
385                ]}
386                onPress={() => router.navigate(`/manga/${featuredManga.id}`)}
387              >
388                <Text style={styles.readNowText}>Read Now</Text>
389              </TouchableOpacity>
390            </View>
391          </LinearGradient>
392        </TouchableOpacity>
393      );
394    }, [featuredManga, insets.top, router, themeColors.primary]);
395  
396    if (isLoading) {
397      return (
398        <View
399          style={[styles.container, { backgroundColor: themeColors.background }]}
400        >
401          <ScrollView
402            showsVerticalScrollIndicator={false}
403            contentContainerStyle={[styles.content]}
404          >
405            <FeaturedMangaSkeleton />
406  
407            <View style={styles.section}>
408              {renderSectionTitle('Continue Reading', 'book')}
409              <RecentlyReadSkeleton />
410            </View>
411  
412            <View style={styles.section}>
413              {renderSectionTitle('Trending Now', 'trophy')}
414              <TrendingSkeleton />
415            </View>
416  
417            <View style={styles.section}>
418              {renderSectionTitle('New Releases', 'sparkles')}
419              <NewReleasesSkeleton />
420            </View>
421          </ScrollView>
422        </View>
423      );
424    }
425  
426    return (
427      <View
428        style={[styles.container, { backgroundColor: themeColors.background }]}
429      >
430        <ScrollView
431          showsVerticalScrollIndicator={false}
432          contentContainerStyle={[styles.content]}
433          refreshControl={
434            <SmoothRefreshControl
435              refreshing={isRefreshing}
436              onRefresh={handleRefresh}
437            />
438          }
439        >
440          {error ? (
441            <View
442              style={[styles.errorContainer, { paddingTop: insets.top + 20 }]}
443            >
444              <Ionicons
445                name="alert-circle-outline"
446                size={48}
447                color={themeColors.notification}
448              />
449              <Text
450                style={[styles.errorText, { color: themeColors.notification }]}
451              >
452                {error}
453              </Text>
454              <TouchableOpacity
455                style={[
456                  styles.retryButton,
457                  { backgroundColor: themeColors.primary },
458                ]}
459                onPress={fetchMangaData}
460              >
461                <Text style={styles.retryButtonText}>Retry</Text>
462              </TouchableOpacity>
463            </View>
464          ) : (
465            <>
466              <PageTransition transitionType="fade" duration={400}>
467                {renderFeaturedManga()}
468              </PageTransition>
469  
470              <PageTransition transitionType="slide" duration={400} delay={100}>
471                <View style={styles.section}>
472                  {renderSectionTitle('Continue Reading', 'book')}
473                  {renderContinueReadingSection()}
474                </View>
475              </PageTransition>
476  
477              <PageTransition transitionType="slide" duration={400} delay={200}>
478                <View style={styles.section}>
479                  {renderSectionTitle('Trending Now', 'trophy')}
480                  <FlatList
481                    data={mostViewedManga.slice(1)}
482                    renderItem={renderTrendingItem}
483                    keyExtractor={(item) => item.id}
484                    horizontal
485                    showsHorizontalScrollIndicator={false}
486                    contentContainerStyle={styles.trendingList}
487                    decelerationRate="fast"
488                    snapToInterval={TRENDING_CARD_WIDTH + 12}
489                    snapToAlignment="start"
490                    getItemLayout={(_data, index) => ({
491                      length: TRENDING_CARD_WIDTH + 12,
492                      offset: (TRENDING_CARD_WIDTH + 12) * index,
493                      index,
494                    })}
495                    removeClippedSubviews={true}
496                    maxToRenderPerBatch={5}
497                    updateCellsBatchingPeriod={100}
498                    windowSize={10}
499                  />
500                </View>
501              </PageTransition>
502  
503              <PageTransition transitionType="slide" duration={400} delay={300}>
504                <View style={styles.section}>
505                  {renderSectionTitle('New Releases', 'sparkles')}
506                  {renderNewReleaseGrid()}
507                </View>
508              </PageTransition>
509            </>
510          )}
511        </ScrollView>
512      </View>
513    );
514  }
515  
516  const styles = StyleSheet.create({
517    container: {
518      flex: 1,
519    },
520    content: {
521      paddingBottom: 100,
522    },
523    section: {
524      marginBottom: 24,
525    },
526    sectionTitleContainer: {
527      flexDirection: 'row',
528      alignItems: 'center',
529      paddingHorizontal: 16,
530      marginBottom: 16,
531    },
532    iconBackground: {
533      width: 36,
534      height: 36,
535      borderRadius: 18,
536      alignItems: 'center',
537      justifyContent: 'center',
538      marginRight: 12,
539    },
540    sectionTitle: {
541      fontSize: 20,
542      fontWeight: 'bold',
543    },
544    featuredContainer: {
545      height: FEATURED_HEIGHT,
546      marginHorizontal: 16,
547      marginBottom: 24,
548      borderRadius: 16,
549      overflow: 'hidden',
550      shadowColor: '#000',
551      shadowOffset: { width: 0, height: 4 },
552      shadowOpacity: 0.2,
553      shadowRadius: 8,
554      elevation: 8,
555    },
556    featuredImage: {
557      width: '100%',
558      height: '100%',
559      resizeMode: 'cover',
560    },
561    featuredGradient: {
562      position: 'absolute',
563      bottom: 0,
564      left: 0,
565      right: 0,
566      height: '70%',
567      justifyContent: 'flex-end',
568      padding: 16,
569    },
570    featuredContent: {
571      alignItems: 'flex-start',
572    },
573    featuredBadge: {
574      flexDirection: 'row',
575      alignItems: 'center',
576      backgroundColor: 'rgba(255, 59, 48, 0.8)',
577      paddingHorizontal: 10,
578      paddingVertical: 5,
579      borderRadius: 12,
580      marginBottom: 8,
581    },
582    featuredBadgeText: {
583      color: '#FFFFFF',
584      fontSize: 12,
585      fontWeight: 'bold',
586      marginLeft: 4,
587    },
588    featuredTitle: {
589      color: '#FFFFFF',
590      fontSize: 22,
591      fontWeight: 'bold',
592      marginBottom: 12,
593      textShadowColor: 'rgba(0, 0, 0, 0.7)',
594      textShadowOffset: { width: 0, height: 1 },
595      textShadowRadius: 3,
596    },
597    readNowButton: {
598      paddingHorizontal: 20,
599      paddingVertical: 8,
600      borderRadius: 20,
601    },
602    readNowText: {
603      color: '#FFFFFF',
604      fontSize: 14,
605      fontWeight: 'bold',
606    },
607    trendingList: {
608      paddingRight: 16,
609      paddingBottom: 8,
610    },
611    trendingItem: {
612      width: TRENDING_CARD_WIDTH,
613      height: TRENDING_CARD_HEIGHT,
614      borderRadius: 16,
615      overflow: 'hidden',
616      shadowColor: '#000',
617      shadowOffset: { width: 0, height: 3 },
618      shadowOpacity: 0.15,
619      shadowRadius: 8,
620      elevation: 6,
621    },
622    trendingImage: {
623      width: '100%',
624      height: '100%',
625      resizeMode: 'cover',
626    },
627    trendingGradient: {
628      position: 'absolute',
629      bottom: 0,
630      left: 0,
631      right: 0,
632      height: '50%',
633      justifyContent: 'flex-end',
634      padding: 12,
635    },
636    trendingContent: {
637      flexDirection: 'row',
638      justifyContent: 'space-between',
639      alignItems: 'flex-end',
640    },
641    trendingTitle: {
642      flex: 1,
643      color: '#FFFFFF',
644      fontSize: 16,
645      fontWeight: 'bold',
646      marginRight: 8,
647      textShadowColor: 'rgba(0, 0, 0, 0.7)',
648      textShadowOffset: { width: 0, height: 1 },
649      textShadowRadius: 3,
650    },
651    rankBadge: {
652      paddingHorizontal: 10,
653      paddingVertical: 4,
654      borderRadius: 12,
655      justifyContent: 'center',
656      alignItems: 'center',
657    },
658    rankText: {
659      color: '#FFFFFF',
660      fontSize: 12,
661      fontWeight: 'bold',
662    },
663    newReleaseGrid: {
664      flexDirection: 'row',
665      flexWrap: 'wrap',
666      paddingHorizontal: 16,
667    },
668    newReleaseWrapper: {
669      width: '50%',
670      padding: 8,
671    },
672    newReleaseCard: {
673      borderRadius: 12,
674      overflow: 'hidden',
675      shadowColor: '#000',
676      shadowOffset: { width: 0, height: 2 },
677      shadowOpacity: 0.1,
678      shadowRadius: 4,
679      elevation: 3,
680    },
681    card: {
682      width: '100%',
683      aspectRatio: 3 / 4,
684      borderRadius: 12,
685    },
686    titleContainer: {
687      marginTop: 8,
688      paddingHorizontal: 4,
689      paddingBottom: 4,
690    },
691    mangaTitle: {
692      fontSize: 14,
693      fontWeight: 'bold',
694    },
695    emptyStateContainer: {
696      marginHorizontal: 16,
697      borderRadius: 16,
698      padding: 24,
699      alignItems: 'center',
700      justifyContent: 'center',
701    },
702    emptyStateText: {
703      fontSize: 16,
704      textAlign: 'center',
705      marginTop: 12,
706      marginBottom: 16,
707    },
708    browseButton: {
709      paddingHorizontal: 20,
710      paddingVertical: 10,
711      borderRadius: 20,
712    },
713    browseButtonText: {
714      fontSize: 14,
715      fontWeight: 'bold',
716    },
717    loadingContainer: {
718      flex: 1,
719      justifyContent: 'center',
720      alignItems: 'center',
721    },
722    loadingIndicator: {
723      marginTop: 20,
724    },
725    loadingText: {
726      marginTop: 16,
727      fontSize: 16,
728    },
729    errorContainer: {
730      flex: 1,
731      alignItems: 'center',
732      justifyContent: 'center',
733      padding: 20,
734    },
735    errorText: {
736      fontSize: 16,
737      textAlign: 'center',
738      marginTop: 12,
739      marginBottom: 20,
740    },
741    retryButton: {
742      paddingHorizontal: 30,
743      paddingVertical: 10,
744      borderRadius: 8,
745      elevation: 2,
746    },
747    retryButtonText: {
748      color: '#FFFFFF',
749      fontSize: 16,
750      fontWeight: 'bold',
751    },
752    recentlyReadList: {
753      paddingRight: 16,
754      paddingBottom: 8,
755    },
756    recentlyReadItem: {
757      width: RECENTLY_READ_CARD_WIDTH,
758      marginRight: 12,
759    },
760    recentlyReadCard: {
761      width: '100%',
762      aspectRatio: 3 / 4,
763      borderRadius: 12,
764      overflow: 'hidden',
765    },
766    recentlyReadTitle: {
767      fontSize: 12,
768      fontWeight: 'bold',
769      marginTop: 6,
770      textAlign: 'center',
771    },
772  });