diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index cfbc1e2..093a4a0 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,6 +1,7 @@ import { Tabs } from 'expo-router'; import React from 'react'; import { Platform } from 'react-native'; +import { Image } from 'react-native'; import { HapticTab } from '@/components/HapticTab'; import { IconSymbol } from '@/components/ui/IconSymbol'; @@ -29,15 +30,35 @@ export default function TabLayout() { , + title: 'Guard', + tabBarIcon: ({ focused }) => ( + + ), }} /> , + title: 'News', + tabBarIcon: ({ focused }) => ( + + ), }} /> diff --git a/app/(tabs)/explore.tsx b/app/(tabs)/explore.tsx index 06e70c4..eda7cc8 100644 --- a/app/(tabs)/explore.tsx +++ b/app/(tabs)/explore.tsx @@ -1,109 +1,781 @@ -import { StyleSheet, Image, Platform } from 'react-native'; -import { Collapsible } from '@/components/Collapsible'; -import { ExternalLink } from '@/components/ExternalLink'; -import ParallaxScrollView from '@/components/ParallaxScrollView'; -import { ThemedText } from '@/components/ThemedText'; -import { ThemedView } from '@/components/ThemedView'; -import { IconSymbol } from '@/components/ui/IconSymbol'; +import { + View, + Text, + Image, + StyleSheet, + 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() { - return ( - - }> - - Explore - - This app includes example code to help you get started. - - - This app has two screens:{' '} - app/(tabs)/index.tsx and{' '} - app/(tabs)/explore.tsx - - - The layout file in app/(tabs)/_layout.tsx{' '} - sets up the tab navigator. - - - Learn more - - - - - You can open this project on Android, iOS, and the web. To open the web version, press{' '} - w in the terminal running this project. - - - - - For static images, you can use the @2x and{' '} - @3x suffixes to provide files for - different screen densities - - - - Learn more - - - - - Open app/_layout.tsx to see how to load{' '} - - custom fonts such as this one. - - - - Learn more - - - - - This template has light and dark mode support. The{' '} - useColorScheme() hook lets you inspect - what the user's current color scheme is, and so you can adjust UI colors accordingly. - - - Learn more - - - - - This template includes an example of an animated component. The{' '} - components/HelloWave.tsx component uses - the powerful react-native-reanimated{' '} - library to create a waving hand animation. - - {Platform.select({ - ios: ( - - The components/ParallaxScrollView.tsx{' '} - component provides a parallax effect for the header image. - +const { width, height } = Dimensions.get("window"); + +// API endpoint - use your own IP address here +const API_URL = "https://dev.dashboard.planpostai.com/news/api/news"; + +// TypeScript interfaces +interface Paragraph { + content: string; +} + +interface NewsItem { + id: number; + created_at: string; + title: string; + description: string; + image_url: string; + image_path?: string; + paragraphs: string[]; +} + +interface AdItem { + id: string; + isAd: boolean; + adIndex: number; +} + +type FeedItem = NewsItem | AdItem; + +// Type guard to check if an item is an ad +const isAd = (item: FeedItem): item is AdItem => { + return (item as AdItem).isAd === true; +}; + +// Single ad data +const singleAd = { + id: 1, + title: "Internet Packages", + description: + "Get the latest data offers...", + image: + "https://cdn01da.grameenphone.com/sites/default/files/2023-09/GP_013_New_Number_Series-1060-x-764.jpg", + url: "https://www.premium-headphones.com", +}; + +export default function NewsScreen(): JSX.Element { + const colorScheme = useColorScheme(); + const router = useRouter(); + const insets = useSafeAreaInsets(); + const [selectedNews, setSelectedNews] = useState(null); + const [modalVisible, setModalVisible] = useState(false); + const [newsData, setNewsData] = useState([]); + const [processedData, setProcessedData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Fetch news data from API + useEffect(() => { + fetchNews(); + }, []); + + // Process news data to add ads after every 4 news items + useEffect(() => { + const processed: FeedItem[] = []; + + newsData.forEach((item, index) => { + processed.push(item); + if ((index + 1) % 4 === 0 && index !== newsData.length - 1) { + processed.push({ + id: `ad-${Math.floor(index / 4)}`, + isAd: true, + adIndex: Math.floor(index / 4), + }); + } + }); + + setProcessedData(processed); + }, [newsData]); + + const fetchNews = async (): Promise => { + setLoading(true); + 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), + ], + })); + + 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 ( + + + SPONSORED + + + {/* Full screen ad with vertical layout */} + navigateToAdPage(singleAd.url)} + activeOpacity={0.9} + > + {/* Image first */} + + + + + {/* Content below image */} + + {singleAd.title} + + {singleAd.description} + + + + + ); + }; + + const renderNewsItem = ({ item }: { item: FeedItem }): JSX.Element => { + if (isAd(item)) { + return ; + } + + // Truncate description to 99% and add ellipsis + const truncatedDescription = truncateDescription(item.paragraphs[0]); + + return ( + + {/* News image - 30% of screen height */} + + + + + {/* Content area - 70% of screen height */} + + {Platform.OS === "ios" ? ( + + + + {formatDate(item.created_at)} + + {/* Title first as requested */} + {item.title} + + {/* Show 99% of description with ellipsis */} + {truncatedDescription} + + {/* Read more button positioned on the right */} + + openNewsModal(item)} + > + Read more + + + + + + + ) : ( + + + + {formatDate(item.created_at)} + + {/* Title first as requested */} + {item.title} + + {/* Show 99% of description with ellipsis */} + {truncatedDescription} + + {/* Read more button positioned on the right */} + + openNewsModal(item)} + > + Read more + + + + + + + )} + + + ); + }; + + const renderErrorView = (): JSX.Element => ( + + + + Connection Error + + + {error} + + + Retry + + + ); + + return ( + + + + {/* Custom Navbar */} + + + router.back()} + > + + + + + + 30 Second + + News + + + + + + + + + {/* Loading Indicator */} + {loading ? ( + + + + Loading news... + + + ) : error ? ( + renderErrorView() + ) : newsData.length === 0 ? ( + + + + No News Found + + + We couldn't find any news articles. Please try again later. + + + Refresh + + + ) : ( + /* Show all news items with ads after every 4 items */ + item.id.toString()} + pagingEnabled + showsVerticalScrollIndicator={false} + snapToAlignment="start" + decelerationRate="fast" + snapToInterval={height} + /> + )} + + {/* News Detail Modal with text X close button */} + + + + {/* Text X close button instead of icon */} + + X + + + {/* Modal Content - Title and Description */} + {selectedNews && ( + + {/* Title */} + {selectedNews.title} + + {/* Date */} + + {formatDate(selectedNews.created_at)} + + + {/* All paragraphs */} + {selectedNews.paragraphs.map((paragraph, index) => ( + + {paragraph} + + ))} + + {/* Extra padding at the bottom for better scrolling */} + + + )} + + + + ); } const styles = StyleSheet.create({ - headerImage: { - color: '#808080', - bottom: -90, - left: -35, - position: 'absolute', + container: { + flex: 1, }, - titleContainer: { - flexDirection: 'row', - gap: 8, + navbar: { + flexDirection: "row", + 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", + }, +}); \ No newline at end of file diff --git a/assets/images/gp.jpeg b/assets/images/gp.jpeg new file mode 100644 index 0000000..7a9fc99 Binary files /dev/null and b/assets/images/gp.jpeg differ diff --git a/assets/images/guard.jpg b/assets/images/guard.jpg new file mode 100644 index 0000000..5d0f8dd Binary files /dev/null and b/assets/images/guard.jpg differ diff --git a/assets/images/news.jpeg b/assets/images/news.jpeg new file mode 100644 index 0000000..c40c0fc Binary files /dev/null and b/assets/images/news.jpeg differ