This commit is contained in:
smfahim25 2025-04-13 10:04:53 +06:00
parent 32402275ac
commit 3fcbffd986
5 changed files with 797 additions and 104 deletions

View file

@ -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() {
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />,
title: 'Guard',
tabBarIcon: ({ focused }) => (
<Image
source={
focused
? require('@/assets/images/guard.jpg')
: require('@/assets/images/guard.jpg')
}
style={{ width: 90, height: 28 }}
resizeMode="contain"
/>
),
}}
/>
<Tabs.Screen
name="explore"
options={{
title: 'Explore',
tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />,
title: 'News',
tabBarIcon: ({ focused }) => (
<Image
source={
focused
? require('@/assets/images/news.jpeg')
: require('@/assets/images/news.jpeg')
}
style={{ width: 40, height: 28 }}
resizeMode="contain"
/>
),
}}
/>
</Tabs>

View file

@ -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 (
<ParallaxScrollView
headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
headerImage={
<IconSymbol
size={310}
color="#808080"
name="chevron.left.forwardslash.chevron.right"
style={styles.headerImage}
/>
}>
<ThemedView style={styles.titleContainer}>
<ThemedText type="title">Explore</ThemedText>
</ThemedView>
<ThemedText>This app includes example code to help you get started.</ThemedText>
<Collapsible title="File-based routing">
<ThemedText>
This app has two screens:{' '}
<ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '}
<ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText>
</ThemedText>
<ThemedText>
The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '}
sets up the tab navigator.
</ThemedText>
<ExternalLink href="https://docs.expo.dev/router/introduction">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Android, iOS, and web support">
<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.
</ThemedText>
</Collapsible>
<Collapsible title="Images">
<ThemedText>
For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '}
<ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
different screen densities
</ThemedText>
<Image source={require('@/assets/images/react-logo.png')} style={{ alignSelf: 'center' }} />
<ExternalLink href="https://reactnative.dev/docs/images">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Custom fonts">
<ThemedText>
Open <ThemedText type="defaultSemiBold">app/_layout.tsx</ThemedText> to see how to load{' '}
<ThemedText style={{ fontFamily: 'SpaceMono' }}>
custom fonts such as this one.
</ThemedText>
</ThemedText>
<ExternalLink href="https://docs.expo.dev/versions/latest/sdk/font">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Light and dark mode components">
<ThemedText>
This template has light and dark mode support. The{' '}
<ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect
what the user's current color scheme is, and so you can adjust UI colors accordingly.
</ThemedText>
<ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Animations">
<ThemedText>
This template includes an example of an animated component. The{' '}
<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({
ios: (
<ThemedText>
The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '}
component provides a parallax effect for the header image.
</ThemedText>
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
),
})}
</Collapsible>
</ParallaxScrollView>
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({
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",
},
});

BIN
assets/images/gp.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
assets/images/guard.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
assets/images/news.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB