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 { 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>

View file

@ -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

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