update
This commit is contained in:
		
							parent
							
								
									32402275ac
								
							
						
					
					
						commit
						3fcbffd986
					
				
					 5 changed files with 797 additions and 104 deletions
				
			
		|  | @ -1,6 +1,7 @@ | ||||||
| import { Tabs } from 'expo-router'; | import { Tabs } from 'expo-router'; | ||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import { Platform } from 'react-native'; | import { Platform } from 'react-native'; | ||||||
|  | import { Image } from 'react-native'; | ||||||
| 
 | 
 | ||||||
| import { HapticTab } from '@/components/HapticTab'; | import { HapticTab } from '@/components/HapticTab'; | ||||||
| import { IconSymbol } from '@/components/ui/IconSymbol'; | import { IconSymbol } from '@/components/ui/IconSymbol'; | ||||||
|  | @ -29,15 +30,35 @@ export default function TabLayout() { | ||||||
|       <Tabs.Screen |       <Tabs.Screen | ||||||
|         name="index" |         name="index" | ||||||
|         options={{ |         options={{ | ||||||
|           title: 'Home', |           title: 'Guard', | ||||||
|           tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />, |          tabBarIcon: ({ focused }) => ( | ||||||
|  |                <Image | ||||||
|  |                  source={ | ||||||
|  |                    focused | ||||||
|  |                      ? require('@/assets/images/guard.jpg') | ||||||
|  |                      : require('@/assets/images/guard.jpg') | ||||||
|  |                  } | ||||||
|  |                  style={{ width: 90, height: 28 }} | ||||||
|  |                  resizeMode="contain" | ||||||
|  |                /> | ||||||
|  |              ), | ||||||
|         }} |         }} | ||||||
|       /> |       /> | ||||||
|       <Tabs.Screen |       <Tabs.Screen | ||||||
|         name="explore" |         name="explore" | ||||||
|         options={{ |         options={{ | ||||||
|           title: 'Explore', |           title: 'News', | ||||||
|           tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />, |           tabBarIcon: ({ focused }) => ( | ||||||
|  |                          <Image | ||||||
|  |                            source={ | ||||||
|  |                              focused | ||||||
|  |                                ? require('@/assets/images/news.jpeg') | ||||||
|  |                                : require('@/assets/images/news.jpeg') | ||||||
|  |                            } | ||||||
|  |                            style={{ width: 40, height: 28 }} | ||||||
|  |                            resizeMode="contain" | ||||||
|  |                          /> | ||||||
|  |                        ), | ||||||
|         }} |         }} | ||||||
|       /> |       /> | ||||||
|     </Tabs> |     </Tabs> | ||||||
|  |  | ||||||
|  | @ -1,109 +1,781 @@ | ||||||
| import { StyleSheet, Image, Platform } from 'react-native'; |  | ||||||
| 
 | 
 | ||||||
| import { Collapsible } from '@/components/Collapsible'; | import { | ||||||
| import { ExternalLink } from '@/components/ExternalLink'; |   View, | ||||||
| import ParallaxScrollView from '@/components/ParallaxScrollView'; |   Text, | ||||||
| import { ThemedText } from '@/components/ThemedText'; |   Image, | ||||||
| import { ThemedView } from '@/components/ThemedView'; |   StyleSheet, | ||||||
| import { IconSymbol } from '@/components/ui/IconSymbol'; |   Dimensions, | ||||||
|  |   Platform, | ||||||
|  |   StatusBar, | ||||||
|  |   FlatList, | ||||||
|  |   TouchableOpacity, | ||||||
|  |   SafeAreaView, | ||||||
|  |   Modal, | ||||||
|  |   ScrollView, | ||||||
|  |   ActivityIndicator, | ||||||
|  |   Linking, | ||||||
|  | } from "react-native"; | ||||||
|  | import { useState, useEffect } from "react"; | ||||||
|  | import { BlurView } from "expo-blur"; | ||||||
|  | import { useRouter } from "expo-router"; | ||||||
|  | import { IconSymbol } from "@/components/ui/IconSymbol"; | ||||||
|  | import { useColorScheme } from "@/hooks/useColorScheme"; | ||||||
|  | import { useSafeAreaInsets } from "react-native-safe-area-context"; | ||||||
| 
 | 
 | ||||||
| export default function TabTwoScreen() { | const { width, height } = Dimensions.get("window"); | ||||||
|   return ( | 
 | ||||||
|     <ParallaxScrollView | // API endpoint - use your own IP address here
 | ||||||
|       headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }} | const API_URL = "https://dev.dashboard.planpostai.com/news/api/news"; | ||||||
|       headerImage={ | 
 | ||||||
|         <IconSymbol | // TypeScript interfaces
 | ||||||
|           size={310} | interface Paragraph { | ||||||
|           color="#808080" |   content: string; | ||||||
|           name="chevron.left.forwardslash.chevron.right" | } | ||||||
|           style={styles.headerImage} | 
 | ||||||
|         /> | interface NewsItem { | ||||||
|       }> |   id: number; | ||||||
|       <ThemedView style={styles.titleContainer}> |   created_at: string; | ||||||
|         <ThemedText type="title">Explore</ThemedText> |   title: string; | ||||||
|       </ThemedView> |   description: string; | ||||||
|       <ThemedText>This app includes example code to help you get started.</ThemedText> |   image_url: string; | ||||||
|       <Collapsible title="File-based routing"> |   image_path?: string; | ||||||
|         <ThemedText> |   paragraphs: string[]; | ||||||
|           This app has two screens:{' '} | } | ||||||
|           <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '} | 
 | ||||||
|           <ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText> | interface AdItem { | ||||||
|         </ThemedText> |   id: string; | ||||||
|         <ThemedText> |   isAd: boolean; | ||||||
|           The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '} |   adIndex: number; | ||||||
|           sets up the tab navigator. | } | ||||||
|         </ThemedText> | 
 | ||||||
|         <ExternalLink href="https://docs.expo.dev/router/introduction"> | type FeedItem = NewsItem | AdItem; | ||||||
|           <ThemedText type="link">Learn more</ThemedText> | 
 | ||||||
|         </ExternalLink> | // Type guard to check if an item is an ad
 | ||||||
|       </Collapsible> | const isAd = (item: FeedItem): item is AdItem => { | ||||||
|       <Collapsible title="Android, iOS, and web support"> |   return (item as AdItem).isAd === true; | ||||||
|         <ThemedText> | }; | ||||||
|           You can open this project on Android, iOS, and the web. To open the web version, press{' '} | 
 | ||||||
|           <ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project. | // Single ad data
 | ||||||
|         </ThemedText> | const singleAd = { | ||||||
|       </Collapsible> |   id: 1, | ||||||
|       <Collapsible title="Images"> |   title: "Internet Packages", | ||||||
|         <ThemedText> |   description: | ||||||
|           For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '} |     "Get the latest data offers...", | ||||||
|           <ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for |   image: | ||||||
|           different screen densities |     "https://cdn01da.grameenphone.com/sites/default/files/2023-09/GP_013_New_Number_Series-1060-x-764.jpg", | ||||||
|         </ThemedText> |   url: "https://www.premium-headphones.com", | ||||||
|         <Image source={require('@/assets/images/react-logo.png')} style={{ alignSelf: 'center' }} /> | }; | ||||||
|         <ExternalLink href="https://reactnative.dev/docs/images"> | 
 | ||||||
|           <ThemedText type="link">Learn more</ThemedText> | export default function NewsScreen(): JSX.Element { | ||||||
|         </ExternalLink> |   const colorScheme = useColorScheme(); | ||||||
|       </Collapsible> |   const router = useRouter(); | ||||||
|       <Collapsible title="Custom fonts"> |   const insets = useSafeAreaInsets(); | ||||||
|         <ThemedText> |   const [selectedNews, setSelectedNews] = useState<NewsItem | null>(null); | ||||||
|           Open <ThemedText type="defaultSemiBold">app/_layout.tsx</ThemedText> to see how to load{' '} |   const [modalVisible, setModalVisible] = useState<boolean>(false); | ||||||
|           <ThemedText style={{ fontFamily: 'SpaceMono' }}> |   const [newsData, setNewsData] = useState<NewsItem[]>([]); | ||||||
|             custom fonts such as this one. |   const [processedData, setProcessedData] = useState<FeedItem[]>([]); | ||||||
|           </ThemedText> |   const [loading, setLoading] = useState<boolean>(true); | ||||||
|         </ThemedText> |   const [error, setError] = useState<string | null>(null); | ||||||
|         <ExternalLink href="https://docs.expo.dev/versions/latest/sdk/font"> | 
 | ||||||
|           <ThemedText type="link">Learn more</ThemedText> |   // Fetch news data from API
 | ||||||
|         </ExternalLink> |   useEffect(() => { | ||||||
|       </Collapsible> |     fetchNews(); | ||||||
|       <Collapsible title="Light and dark mode components"> |   }, []); | ||||||
|         <ThemedText> | 
 | ||||||
|           This template has light and dark mode support. The{' '} |   // Process news data to add ads after every 4 news items
 | ||||||
|           <ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect |   useEffect(() => { | ||||||
|           what the user's current color scheme is, and so you can adjust UI colors accordingly. |     const processed: FeedItem[] = []; | ||||||
|         </ThemedText> | 
 | ||||||
|         <ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/"> |     newsData.forEach((item, index) => { | ||||||
|           <ThemedText type="link">Learn more</ThemedText> |       processed.push(item); | ||||||
|         </ExternalLink> |       if ((index + 1) % 4 === 0 && index !== newsData.length - 1) { | ||||||
|       </Collapsible> |         processed.push({ | ||||||
|       <Collapsible title="Animations"> |           id: `ad-${Math.floor(index / 4)}`, | ||||||
|         <ThemedText> |           isAd: true, | ||||||
|           This template includes an example of an animated component. The{' '} |           adIndex: Math.floor(index / 4), | ||||||
|           <ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses |         }); | ||||||
|           the powerful <ThemedText type="defaultSemiBold">react-native-reanimated</ThemedText>{' '} |       } | ||||||
|           library to create a waving hand animation. |     }); | ||||||
|         </ThemedText> | 
 | ||||||
|         {Platform.select({ |     setProcessedData(processed); | ||||||
|           ios: ( |   }, [newsData]); | ||||||
|             <ThemedText> | 
 | ||||||
|               The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '} |   const fetchNews = async (): Promise<void> => { | ||||||
|               component provides a parallax effect for the header image. |     setLoading(true); | ||||||
|             </ThemedText> |     setError(null); | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       // Set a timeout to prevent hanging requests
 | ||||||
|  |       const controller = new AbortController(); | ||||||
|  |       const timeoutId = setTimeout(() => controller.abort(), 10000); | ||||||
|  | 
 | ||||||
|  |       const response = await fetch(API_URL, { | ||||||
|  |         signal: controller.signal, | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       clearTimeout(timeoutId); | ||||||
|  | 
 | ||||||
|  |       if (!response.ok) { | ||||||
|  |         throw new Error(`HTTP error! Status: ${response.status}`); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const responseJson = await response.json(); | ||||||
|  | 
 | ||||||
|  |       // Extract data from the nested structure
 | ||||||
|  |       const data = responseJson.data || responseJson; | ||||||
|  | 
 | ||||||
|  |       if (!data || !Array.isArray(data)) { | ||||||
|  |         throw new Error("Invalid data format received from API"); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // Process the data to ensure it has the required format
 | ||||||
|  |       const processedData: NewsItem[] = data.map((item: any) => ({ | ||||||
|  |         ...item, | ||||||
|  |         // If paragraphs don't exist, create them from description
 | ||||||
|  |         paragraphs: item.paragraphs || [ | ||||||
|  |           item.description.substring(0, item.description.length / 3), | ||||||
|  |           item.description.substring( | ||||||
|  |             item.description.length / 3, | ||||||
|  |             (2 * item.description.length) / 3 | ||||||
|           ), |           ), | ||||||
|         })} |           item.description.substring((2 * item.description.length) / 3), | ||||||
|       </Collapsible> |         ], | ||||||
|     </ParallaxScrollView> |       })); | ||||||
|  | 
 | ||||||
|  |       setNewsData(processedData); | ||||||
|  |     } catch (err) { | ||||||
|  |       console.error("Failed to fetch news:", err); | ||||||
|  |       setError( | ||||||
|  |         err instanceof Error ? err.message : "An unknown error occurred" | ||||||
|  |       ); | ||||||
|  |       setNewsData([]); | ||||||
|  |     } finally { | ||||||
|  |       setLoading(false); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const formatDate = (dateString: string): string => { | ||||||
|  |     const date = new Date(dateString); | ||||||
|  |     return date.toLocaleDateString("en-US", { | ||||||
|  |       year: "numeric", | ||||||
|  |       month: "long", | ||||||
|  |       day: "numeric", | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   // Function to truncate text to 99% and add ellipsis
 | ||||||
|  |   const truncateDescription = (text: string): string => { | ||||||
|  |     const truncateLength = Math.floor(text.length * 0.99); | ||||||
|  |     return text.substring(0, truncateLength) + "..."; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const openNewsModal = (news: NewsItem): void => { | ||||||
|  |     setSelectedNews(news); | ||||||
|  |     setModalVisible(true); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const closeNewsModal = (): void => { | ||||||
|  |     setModalVisible(false); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   // Navigate to ad website
 | ||||||
|  |   const navigateToAdPage = (url: string): void => { | ||||||
|  |     // Open the URL in the device's browser
 | ||||||
|  |     Linking.openURL(url).catch((err) => | ||||||
|  |       console.error("Couldn't open URL: ", err) | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   // Advertisement component with full screen layout
 | ||||||
|  |   const AdvertisementCard = ({ index }: { index: number }): JSX.Element => { | ||||||
|  |     return ( | ||||||
|  |       <View style={styles.fullScreenItem}> | ||||||
|  |         <View style={styles.adHeader}> | ||||||
|  |           <Text style={[styles.adLabel, { color: "#fff" }]}>SPONSORED</Text> | ||||||
|  |         </View> | ||||||
|  | 
 | ||||||
|  |         {/* Full screen ad with vertical layout */} | ||||||
|  |         <TouchableOpacity | ||||||
|  |           style={styles.fullScreenAdContainer} | ||||||
|  |           onPress={() => navigateToAdPage(singleAd.url)} | ||||||
|  |           activeOpacity={0.9} | ||||||
|  |         > | ||||||
|  |           {/* Image first */} | ||||||
|  |           <View style={styles.adImageContainer}> | ||||||
|  |             <Image | ||||||
|  |               source={{ uri: singleAd.image }} | ||||||
|  |               style={styles.singleAdImage} | ||||||
|  |               resizeMode="cover" | ||||||
|  |             /> | ||||||
|  |           </View> | ||||||
|  | 
 | ||||||
|  |           {/* Content below image */} | ||||||
|  |           <View style={styles.singleAdContent}> | ||||||
|  |             <Text style={styles.singleAdTitle}>{singleAd.title}</Text> | ||||||
|  |             <Text style={styles.singleAdDescription}> | ||||||
|  |               {singleAd.description} | ||||||
|  |             </Text> | ||||||
|  |           </View> | ||||||
|  |         </TouchableOpacity> | ||||||
|  |       </View> | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const renderNewsItem = ({ item }: { item: FeedItem }): JSX.Element => { | ||||||
|  |     if (isAd(item)) { | ||||||
|  |       return <AdvertisementCard index={item.adIndex} />; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Truncate description to 99% and add ellipsis
 | ||||||
|  |     const truncatedDescription = truncateDescription(item.paragraphs[0]); | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |       <View style={styles.newsItemContainer}> | ||||||
|  |         {/* News image - 30% of screen height */} | ||||||
|  |         <View style={styles.imageContainer}> | ||||||
|  |           <Image | ||||||
|  |             source={{ uri: item.image_url }} | ||||||
|  |             style={styles.newsImage} | ||||||
|  |             resizeMode="cover" | ||||||
|  |           /> | ||||||
|  |         </View> | ||||||
|  | 
 | ||||||
|  |         {/* Content area - 70% of screen height */} | ||||||
|  |         <View style={styles.contentSection}> | ||||||
|  |           {Platform.OS === "ios" ? ( | ||||||
|  |             <BlurView | ||||||
|  |               intensity={80} | ||||||
|  |               tint="dark" | ||||||
|  |               style={[ | ||||||
|  |                 styles.blurContainer, | ||||||
|  |                 { backgroundColor: "rgba(167, 61, 228, 0.75)" }, | ||||||
|  |               ]} | ||||||
|  |             > | ||||||
|  |               <ScrollView | ||||||
|  |                 style={styles.scrollableContent} | ||||||
|  |                 showsVerticalScrollIndicator={false} | ||||||
|  |               > | ||||||
|  |                 <View style={styles.textContainer}> | ||||||
|  |                   <Text style={styles.date}>{formatDate(item.created_at)}</Text> | ||||||
|  | 
 | ||||||
|  |                   {/* Title first as requested */} | ||||||
|  |                   <Text style={styles.title}>{item.title}</Text> | ||||||
|  | 
 | ||||||
|  |                   {/* Show 99% of description with ellipsis */} | ||||||
|  |                   <Text style={styles.paragraph}>{truncatedDescription}</Text> | ||||||
|  | 
 | ||||||
|  |                   {/* Read more button positioned on the right */} | ||||||
|  |                   <View style={styles.readMoreContainer}> | ||||||
|  |                     <TouchableOpacity | ||||||
|  |                       style={styles.readMoreButton} | ||||||
|  |                       onPress={() => openNewsModal(item)} | ||||||
|  |                     > | ||||||
|  |                       <Text style={styles.readMoreText}>Read more</Text> | ||||||
|  |                       <IconSymbol | ||||||
|  |                         name="chevron.right" | ||||||
|  |                         size={16} | ||||||
|  |                         color="#ffffff" | ||||||
|  |                       /> | ||||||
|  |                     </TouchableOpacity> | ||||||
|  |                   </View> | ||||||
|  |                 </View> | ||||||
|  |               </ScrollView> | ||||||
|  |             </BlurView> | ||||||
|  |           ) : ( | ||||||
|  |             <View style={[styles.blurContainer, styles.androidBlur]}> | ||||||
|  |               <ScrollView | ||||||
|  |                 style={styles.scrollableContent} | ||||||
|  |                 showsVerticalScrollIndicator={false} | ||||||
|  |               > | ||||||
|  |                 <View style={styles.textContainer}> | ||||||
|  |                   <Text style={styles.date}>{formatDate(item.created_at)}</Text> | ||||||
|  | 
 | ||||||
|  |                   {/* Title first as requested */} | ||||||
|  |                   <Text style={styles.title}>{item.title}</Text> | ||||||
|  | 
 | ||||||
|  |                   {/* Show 99% of description with ellipsis */} | ||||||
|  |                   <Text style={styles.paragraph}>{truncatedDescription}</Text> | ||||||
|  | 
 | ||||||
|  |                   {/* Read more button positioned on the right */} | ||||||
|  |                   <View style={styles.readMoreContainer}> | ||||||
|  |                     <TouchableOpacity | ||||||
|  |                       style={styles.readMoreButton} | ||||||
|  |                       onPress={() => openNewsModal(item)} | ||||||
|  |                     > | ||||||
|  |                       <Text style={styles.readMoreText}>Read more</Text> | ||||||
|  |                       <IconSymbol | ||||||
|  |                         name="chevron.right" | ||||||
|  |                         size={16} | ||||||
|  |                         color="#ffffff" | ||||||
|  |                       /> | ||||||
|  |                     </TouchableOpacity> | ||||||
|  |                   </View> | ||||||
|  |                 </View> | ||||||
|  |               </ScrollView> | ||||||
|  |             </View> | ||||||
|  |           )} | ||||||
|  |         </View> | ||||||
|  |       </View> | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const renderErrorView = (): JSX.Element => ( | ||||||
|  |     <View | ||||||
|  |       style={[ | ||||||
|  |         styles.errorContainer, | ||||||
|  |         { backgroundColor: colorScheme === "dark" ? "#1e1e1e" : "#ffffff" }, | ||||||
|  |       ]} | ||||||
|  |     > | ||||||
|  |       <IconSymbol name="exclamationmark.triangle" size={50} color="#FF3B30" /> | ||||||
|  |       <Text | ||||||
|  |         style={[ | ||||||
|  |           styles.errorTitle, | ||||||
|  |           { color: colorScheme === "dark" ? "#fff" : "#000" }, | ||||||
|  |         ]} | ||||||
|  |       > | ||||||
|  |         Connection Error | ||||||
|  |       </Text> | ||||||
|  |       <Text | ||||||
|  |         style={[ | ||||||
|  |           styles.errorMessage, | ||||||
|  |           { color: colorScheme === "dark" ? "#ddd" : "#666" }, | ||||||
|  |         ]} | ||||||
|  |       > | ||||||
|  |         {error} | ||||||
|  |       </Text> | ||||||
|  |       <TouchableOpacity style={styles.retryButton} onPress={fetchNews}> | ||||||
|  |         <Text style={styles.retryButtonText}>Retry</Text> | ||||||
|  |       </TouchableOpacity> | ||||||
|  |     </View> | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <View | ||||||
|  |       style={[ | ||||||
|  |         styles.container, | ||||||
|  |         { backgroundColor: colorScheme === "dark" ? "#121212" : "#f0f2f5" }, | ||||||
|  |       ]} | ||||||
|  |     > | ||||||
|  |       <StatusBar | ||||||
|  |         translucent | ||||||
|  |         backgroundColor="transparent" | ||||||
|  |         barStyle={colorScheme === "dark" ? "light-content" : "dark-content"} | ||||||
|  |       /> | ||||||
|  | 
 | ||||||
|  |       {/* Custom Navbar */} | ||||||
|  |       <SafeAreaView | ||||||
|  |         style={{ | ||||||
|  |           backgroundColor: colorScheme === "dark" ? "#121212" : "#ffffff", | ||||||
|  |         }} | ||||||
|  |       > | ||||||
|  |         <View | ||||||
|  |           style={[ | ||||||
|  |             styles.navbar, | ||||||
|  |             { | ||||||
|  |               backgroundColor: colorScheme === "dark" ? "#121212" : "#ffffff", | ||||||
|  |               paddingTop: Platform.OS === "android" ? insets.top + 8 : 8, | ||||||
|  |               paddingBottom: 12, | ||||||
|  |               borderBottomColor: | ||||||
|  |                 colorScheme === "dark" ? "#2c2c2c" : "rgba(150, 150, 150, 0.2)", | ||||||
|  |               borderBottomWidth: 1, | ||||||
|  |             }, | ||||||
|  |           ]} | ||||||
|  |         > | ||||||
|  |           <TouchableOpacity | ||||||
|  |             style={styles.backButton} | ||||||
|  |             onPress={() => router.back()} | ||||||
|  |           > | ||||||
|  |             <IconSymbol | ||||||
|  |               name="chevron.left" | ||||||
|  |               size={24} | ||||||
|  |               color={colorScheme === "dark" ? "#ffffff" : "#000000"} | ||||||
|  |             /> | ||||||
|  |           </TouchableOpacity> | ||||||
|  | 
 | ||||||
|  |           <View style={styles.brandContainer}> | ||||||
|  |             <Text | ||||||
|  |               style={[ | ||||||
|  |                 styles.brandText, | ||||||
|  |                 { color: colorScheme === "dark" ? "#ffffff" : "#000000" }, | ||||||
|  |               ]} | ||||||
|  |             > | ||||||
|  |               30 Second | ||||||
|  |             </Text> | ||||||
|  |             <Text style={styles.brandAccent}>News</Text> | ||||||
|  |           </View> | ||||||
|  | 
 | ||||||
|  |           <TouchableOpacity style={styles.refreshButton} onPress={fetchNews}> | ||||||
|  |             <IconSymbol | ||||||
|  |               name="arrow.clockwise" | ||||||
|  |               size={20} | ||||||
|  |               color={colorScheme === "dark" ? "#ffffff" : "#000000"} | ||||||
|  |             /> | ||||||
|  |           </TouchableOpacity> | ||||||
|  |         </View> | ||||||
|  |       </SafeAreaView> | ||||||
|  | 
 | ||||||
|  |       {/* Loading Indicator */} | ||||||
|  |       {loading ? ( | ||||||
|  |         <View | ||||||
|  |           style={[ | ||||||
|  |             styles.loadingContainer, | ||||||
|  |             { backgroundColor: colorScheme === "dark" ? "#121212" : "#f0f2f5" }, | ||||||
|  |           ]} | ||||||
|  |         > | ||||||
|  |           <ActivityIndicator size="large" color="#007AFF" /> | ||||||
|  |           <Text | ||||||
|  |             style={[ | ||||||
|  |               styles.loadingText, | ||||||
|  |               { color: colorScheme === "dark" ? "#fff" : "#007AFF" }, | ||||||
|  |             ]} | ||||||
|  |           > | ||||||
|  |             Loading news... | ||||||
|  |           </Text> | ||||||
|  |         </View> | ||||||
|  |       ) : error ? ( | ||||||
|  |         renderErrorView() | ||||||
|  |       ) : newsData.length === 0 ? ( | ||||||
|  |         <View | ||||||
|  |           style={[ | ||||||
|  |             styles.errorContainer, | ||||||
|  |             { backgroundColor: colorScheme === "dark" ? "#1e1e1e" : "#ffffff" }, | ||||||
|  |           ]} | ||||||
|  |         > | ||||||
|  |           <IconSymbol name="newspaper" size={50} color="#007AFF" /> | ||||||
|  |           <Text | ||||||
|  |             style={[ | ||||||
|  |               styles.errorTitle, | ||||||
|  |               { color: colorScheme === "dark" ? "#fff" : "#000" }, | ||||||
|  |             ]} | ||||||
|  |           > | ||||||
|  |             No News Found | ||||||
|  |           </Text> | ||||||
|  |           <Text | ||||||
|  |             style={[ | ||||||
|  |               styles.errorMessage, | ||||||
|  |               { color: colorScheme === "dark" ? "#ddd" : "#666" }, | ||||||
|  |             ]} | ||||||
|  |           > | ||||||
|  |             We couldn't find any news articles. Please try again later. | ||||||
|  |           </Text> | ||||||
|  |           <TouchableOpacity style={styles.retryButton} onPress={fetchNews}> | ||||||
|  |             <Text style={styles.retryButtonText}>Refresh</Text> | ||||||
|  |           </TouchableOpacity> | ||||||
|  |         </View> | ||||||
|  |       ) : ( | ||||||
|  |         /* Show all news items with ads after every 4 items */ | ||||||
|  |         <FlatList | ||||||
|  |           data={processedData} | ||||||
|  |           renderItem={renderNewsItem} | ||||||
|  |           keyExtractor={(item) => item.id.toString()} | ||||||
|  |           pagingEnabled | ||||||
|  |           showsVerticalScrollIndicator={false} | ||||||
|  |           snapToAlignment="start" | ||||||
|  |           decelerationRate="fast" | ||||||
|  |           snapToInterval={height} | ||||||
|  |         /> | ||||||
|  |       )} | ||||||
|  | 
 | ||||||
|  |       {/* News Detail Modal with text X close button */} | ||||||
|  |       <Modal | ||||||
|  |         animationType="slide" | ||||||
|  |         transparent={true} | ||||||
|  |         visible={modalVisible} | ||||||
|  |         onRequestClose={closeNewsModal} | ||||||
|  |       > | ||||||
|  |         <View style={styles.modalContainer}> | ||||||
|  |           <View | ||||||
|  |             style={[ | ||||||
|  |               styles.modalContent, | ||||||
|  |               { backgroundColor: "rgba(167, 61, 228, 0.95)" }, | ||||||
|  |             ]} | ||||||
|  |           > | ||||||
|  |             {/* Text X close button instead of icon */} | ||||||
|  |             <TouchableOpacity | ||||||
|  |               style={styles.closeIconButton} | ||||||
|  |               onPress={closeNewsModal} | ||||||
|  |             > | ||||||
|  |               <Text style={styles.closeButtonText}>X</Text> | ||||||
|  |             </TouchableOpacity> | ||||||
|  | 
 | ||||||
|  |             {/* Modal Content - Title and Description */} | ||||||
|  |             {selectedNews && ( | ||||||
|  |               <ScrollView | ||||||
|  |                 style={styles.modalScrollView} | ||||||
|  |                 showsVerticalScrollIndicator={true} | ||||||
|  |                 contentContainerStyle={styles.modalScrollViewContent} | ||||||
|  |               > | ||||||
|  |                 {/* Title */} | ||||||
|  |                 <Text style={styles.modalNewsTitle}>{selectedNews.title}</Text> | ||||||
|  | 
 | ||||||
|  |                 {/* Date */} | ||||||
|  |                 <Text style={styles.modalDate}> | ||||||
|  |                   {formatDate(selectedNews.created_at)} | ||||||
|  |                 </Text> | ||||||
|  | 
 | ||||||
|  |                 {/* All paragraphs */} | ||||||
|  |                 {selectedNews.paragraphs.map((paragraph, index) => ( | ||||||
|  |                   <Text key={index} style={styles.modalParagraph}> | ||||||
|  |                     {paragraph} | ||||||
|  |                   </Text> | ||||||
|  |                 ))} | ||||||
|  | 
 | ||||||
|  |                 {/* Extra padding at the bottom for better scrolling */} | ||||||
|  |                 <View style={styles.modalBottomPadding} /> | ||||||
|  |               </ScrollView> | ||||||
|  |             )} | ||||||
|  |           </View> | ||||||
|  |         </View> | ||||||
|  |       </Modal> | ||||||
|  |     </View> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const styles = StyleSheet.create({ | const styles = StyleSheet.create({ | ||||||
|   headerImage: { |   container: { | ||||||
|     color: '#808080', |     flex: 1, | ||||||
|     bottom: -90, |  | ||||||
|     left: -35, |  | ||||||
|     position: 'absolute', |  | ||||||
|   }, |   }, | ||||||
|   titleContainer: { |   navbar: { | ||||||
|     flexDirection: 'row', |     flexDirection: "row", | ||||||
|     gap: 8, |     alignItems: "center", | ||||||
|  |     justifyContent: "space-between", | ||||||
|  |     paddingHorizontal: 16, | ||||||
|  |   }, | ||||||
|  |   backButton: { | ||||||
|  |     padding: 8, | ||||||
|  |     borderRadius: 20, | ||||||
|  |   }, | ||||||
|  |   brandContainer: { | ||||||
|  |     flexDirection: "row", | ||||||
|  |     alignItems: "center", | ||||||
|  |   }, | ||||||
|  |   brandText: { | ||||||
|  |     fontSize: 20, | ||||||
|  |     fontWeight: "700", | ||||||
|  |   }, | ||||||
|  |   brandAccent: { | ||||||
|  |     fontSize: 20, | ||||||
|  |     fontWeight: "700", | ||||||
|  |     color: "#007AFF", | ||||||
|  |     marginLeft: 4, | ||||||
|  |   }, | ||||||
|  |   refreshButton: { | ||||||
|  |     padding: 8, | ||||||
|  |     borderRadius: 20, | ||||||
|  |   }, | ||||||
|  |   loadingContainer: { | ||||||
|  |     flex: 1, | ||||||
|  |     justifyContent: "center", | ||||||
|  |     alignItems: "center", | ||||||
|  |   }, | ||||||
|  |   loadingText: { | ||||||
|  |     marginTop: 10, | ||||||
|  |     fontSize: 16, | ||||||
|  |     color: "#007AFF", | ||||||
|  |   }, | ||||||
|  |   errorContainer: { | ||||||
|  |     flex: 1, | ||||||
|  |     justifyContent: "center", | ||||||
|  |     alignItems: "center", | ||||||
|  |     padding: 20, | ||||||
|  |   }, | ||||||
|  |   errorTitle: { | ||||||
|  |     fontSize: 20, | ||||||
|  |     fontWeight: "bold", | ||||||
|  |     marginTop: 16, | ||||||
|  |     marginBottom: 8, | ||||||
|  |   }, | ||||||
|  |   errorMessage: { | ||||||
|  |     fontSize: 16, | ||||||
|  |     textAlign: "center", | ||||||
|  |     marginBottom: 20, | ||||||
|  |     color: "#666", | ||||||
|  |   }, | ||||||
|  |   retryButton: { | ||||||
|  |     backgroundColor: "#007AFF", | ||||||
|  |     paddingHorizontal: 20, | ||||||
|  |     paddingVertical: 10, | ||||||
|  |     borderRadius: 8, | ||||||
|  |   }, | ||||||
|  |   retryButtonText: { | ||||||
|  |     color: "#fff", | ||||||
|  |     fontWeight: "600", | ||||||
|  |     fontSize: 16, | ||||||
|  |   }, | ||||||
|  |   newsItemContainer: { | ||||||
|  |     height: height, | ||||||
|  |     width: width, | ||||||
|  |     position: "relative", | ||||||
|  |   }, | ||||||
|  |   imageContainer: { | ||||||
|  |     height: height * 0.3, // 30% of screen height for image
 | ||||||
|  |     width: width, | ||||||
|  |     overflow: "hidden", | ||||||
|  |   }, | ||||||
|  |   newsImage: { | ||||||
|  |     width: "100%", | ||||||
|  |     height: "100%", | ||||||
|  |   }, | ||||||
|  |   contentSection: { | ||||||
|  |     height: height * 0.7, // 70% of screen height for content
 | ||||||
|  |     width: width, | ||||||
|  |   }, | ||||||
|  |   blurContainer: { | ||||||
|  |     flex: 1, | ||||||
|  |     borderTopLeftRadius: 0, | ||||||
|  |     borderTopRightRadius: 0, | ||||||
|  |     overflow: "hidden", | ||||||
|  |   }, | ||||||
|  |   androidBlur: { | ||||||
|  |     backgroundColor: "rgba(167, 61, 228, 0.75)", // Purple background from the provided code
 | ||||||
|  |   }, | ||||||
|  |   scrollableContent: { | ||||||
|  |     flex: 1, | ||||||
|  |   }, | ||||||
|  |   textContainer: { | ||||||
|  |     padding: 20, | ||||||
|  |     paddingTop: 20, | ||||||
|  |     paddingBottom: 30, | ||||||
|  |   }, | ||||||
|  |   date: { | ||||||
|  |     color: "#ccc", | ||||||
|  |     fontSize: 12, | ||||||
|  |     marginBottom: 10, | ||||||
|  |   }, | ||||||
|  |   title: { | ||||||
|  |     color: "#fff", | ||||||
|  |     fontSize: 18, | ||||||
|  |     fontWeight: "bold", | ||||||
|  |     marginBottom: 15, | ||||||
|  |   }, | ||||||
|  |   paragraph: { | ||||||
|  |     color: "#fff", | ||||||
|  |     fontSize: 15, | ||||||
|  |     lineHeight: 22, | ||||||
|  |     marginBottom: 15, | ||||||
|  |   }, | ||||||
|  |   // Read more button styles
 | ||||||
|  |   readMoreContainer: { | ||||||
|  |     flexDirection: "row", | ||||||
|  |     justifyContent: "flex-end", | ||||||
|  |     marginTop: 20, | ||||||
|  |   }, | ||||||
|  |   readMoreButton: { | ||||||
|  |     flexDirection: "row", | ||||||
|  |     alignItems: "center", | ||||||
|  |     backgroundColor: "rgba(255, 255, 255, 0.2)", | ||||||
|  |     paddingVertical: 10, | ||||||
|  |     paddingHorizontal: 16, | ||||||
|  |     borderRadius: 8, | ||||||
|  |   }, | ||||||
|  |   readMoreText: { | ||||||
|  |     color: "#ffffff", | ||||||
|  |     fontWeight: "600", | ||||||
|  |     fontSize: 16, | ||||||
|  |     marginRight: 5, | ||||||
|  |   }, | ||||||
|  |   modalContainer: { | ||||||
|  |     flex: 1, | ||||||
|  |     backgroundColor: "rgba(0,0,0,0.5)", | ||||||
|  |   }, | ||||||
|  |   modalContent: { | ||||||
|  |     flex: 1, | ||||||
|  |     marginTop: 60, // Space from top
 | ||||||
|  |     marginBottom: 20, // Space from bottom
 | ||||||
|  |     marginHorizontal: 15, // Space from sides
 | ||||||
|  |     backgroundColor: "rgba(167, 61, 228, 0.95)", // Purple background
 | ||||||
|  |     borderRadius: 20, | ||||||
|  |     overflow: "hidden", | ||||||
|  |   }, | ||||||
|  |   modalScrollView: { | ||||||
|  |     flex: 1, | ||||||
|  |   }, | ||||||
|  |   modalScrollViewContent: { | ||||||
|  |     padding: 20, | ||||||
|  |     paddingTop: 25, | ||||||
|  |   }, | ||||||
|  |   modalNewsTitle: { | ||||||
|  |     fontSize: 24, | ||||||
|  |     fontWeight: "bold", | ||||||
|  |     marginBottom: 10, | ||||||
|  |     color: "#ffffff", | ||||||
|  |     marginTop: 15, // Add space at the top for the close button
 | ||||||
|  |   }, | ||||||
|  |   modalDate: { | ||||||
|  |     fontSize: 14, | ||||||
|  |     color: "rgba(255, 255, 255, 0.7)", | ||||||
|  |     marginBottom: 20, | ||||||
|  |   }, | ||||||
|  |   modalParagraph: { | ||||||
|  |     fontSize: 17, | ||||||
|  |     lineHeight: 24, | ||||||
|  |     marginBottom: 16, | ||||||
|  |     color: "#ffffff", | ||||||
|  |   }, | ||||||
|  |   closeIconButton: { | ||||||
|  |     position: "absolute", | ||||||
|  |     top: 15, | ||||||
|  |     right: 15, | ||||||
|  |     zIndex: 10, | ||||||
|  |     width: 36, | ||||||
|  |     height: 36, | ||||||
|  |     borderRadius: 18, // Half of width/height
 | ||||||
|  |     backgroundColor: "rgba(255, 255, 255, 0.3)", | ||||||
|  |     justifyContent: "center", | ||||||
|  |     alignItems: "center", | ||||||
|  |   }, | ||||||
|  |   closeButtonText: { | ||||||
|  |     color: "#ffffff", | ||||||
|  |     fontSize: 20, | ||||||
|  |     fontWeight: "bold", | ||||||
|  |   }, | ||||||
|  |   modalBottomPadding: { | ||||||
|  |     height: 40, // Extra padding at the bottom for better scrolling
 | ||||||
|  |   }, | ||||||
|  |   fullScreenItem: { | ||||||
|  |     height: height, | ||||||
|  |     width: width, | ||||||
|  |     backgroundColor: "#ffffff", | ||||||
|  |   }, | ||||||
|  |   adHeader: { | ||||||
|  |     backgroundColor: "#007AFF", | ||||||
|  |     paddingVertical: 6, | ||||||
|  |     paddingHorizontal: 12, | ||||||
|  |     alignItems: "center", | ||||||
|  |   }, | ||||||
|  |   adLabel: { | ||||||
|  |     fontSize: 12, | ||||||
|  |     fontWeight: "bold", | ||||||
|  |     color: "#fff", | ||||||
|  |   }, | ||||||
|  |   // Full screen ad styles
 | ||||||
|  |   fullScreenAdContainer: { | ||||||
|  |     height: height, | ||||||
|  |     width: width, | ||||||
|  |   }, | ||||||
|  |   adImageContainer: { | ||||||
|  |     height: height * 0.5, // 70% for image
 | ||||||
|  |     width: width, | ||||||
|  |   }, | ||||||
|  |   singleAdImage: { | ||||||
|  |     width: "100%", | ||||||
|  |     height: "100%", | ||||||
|  |   }, | ||||||
|  |   singleAdContent: { | ||||||
|  |     height: height * 0.5, // 30% for content
 | ||||||
|  |     width: width, | ||||||
|  |     padding: 20, | ||||||
|  |     backgroundColor: "#ffffff", | ||||||
|  |   }, | ||||||
|  |   singleAdTitle: { | ||||||
|  |     fontSize: 24, | ||||||
|  |     fontWeight: "bold", | ||||||
|  |     marginBottom: 15, | ||||||
|  |     color: "#000", | ||||||
|  |   }, | ||||||
|  |   singleAdDescription: { | ||||||
|  |     fontSize: 16, | ||||||
|  |     lineHeight: 24, | ||||||
|  |     color: "#333", | ||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
							
								
								
									
										
											BIN
										
									
								
								assets/images/gp.jpeg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/images/gp.jpeg
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 81 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/guard.jpg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/images/guard.jpg
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 12 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/news.jpeg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/images/news.jpeg
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 87 KiB | 
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 smfahim25
						smfahim25