fake-checker-nativeapp/app/(tabs)/explore.tsx
2025-04-13 10:04:53 +06:00

781 lines
No EOL
20 KiB
TypeScript

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";
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<NewsItem | null>(null);
const [modalVisible, setModalVisible] = useState<boolean>(false);
const [newsData, setNewsData] = useState<NewsItem[]>([]);
const [processedData, setProcessedData] = useState<FeedItem[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(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<void> => {
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 (
<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({
container: {
flex: 1,
},
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",
},
});