Yaltopia-Tickets-App/app/(tabs)/news.tsx

312 lines
9.4 KiB
TypeScript

import React, { useState, useEffect, useCallback } from "react";
import {
View,
ScrollView,
Pressable,
ActivityIndicator,
FlatList,
Dimensions,
RefreshControl,
} from "react-native";
import { Text } from "@/components/ui/text";
import { Card } from "@/components/ui/card";
import { useSirouRouter } from "@sirou/react-native";
import { AppRoutes } from "@/lib/routes";
import { Newspaper, ChevronRight, Clock } from "@/lib/icons";
import { ScreenWrapper } from "@/components/ScreenWrapper";
import { StandardHeader } from "@/components/StandardHeader";
import { api, newsApi } from "@/lib/api";
import { ShadowWrapper } from "@/components/ShadowWrapper";
const { width } = Dimensions.get("window");
const LATEST_CARD_WIDTH = width * 0.8;
interface NewsItem {
id: string;
title: string;
content: string;
category: "ANNOUNCEMENT" | "UPDATE" | "MAINTENANCE" | "NEWS";
priority: "LOW" | "MEDIUM" | "HIGH";
publishedAt: string;
viewCount: number;
}
export default function NewsScreen() {
const nav = useSirouRouter<AppRoutes>();
// Safe accessor to handle initialization race conditions
const getNewsApi = () => {
if (newsApi) return newsApi;
return api.news;
};
// Latest News State
const [latestNews, setLatestNews] = useState<NewsItem[]>([]);
const [loadingLatest, setLoadingLatest] = useState(true);
// All News State
const [allNews, setAllNews] = useState<NewsItem[]>([]);
const [loadingAll, setLoadingAll] = useState(true);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const fetchLatest = async () => {
try {
setLoadingLatest(true);
const service = getNewsApi();
if (!service) throw new Error("News service unavailable");
const data = await service.getLatest({ query: { limit: 5 } });
setLatestNews(data || []);
} catch (err) {
console.error("[News] Latest fetch error:", err);
} finally {
setLoadingLatest(false);
}
};
const fetchAll = async (pageNum: number, isRefresh = false) => {
try {
if (!isRefresh) {
pageNum === 1 ? setLoadingAll(true) : setLoadingMore(true);
}
const service = getNewsApi();
if (!service) throw new Error("News service unavailable");
const response = await service.getAll({
query: { page: pageNum, limit: 10, isPublished: true },
});
const newData = response.data || [];
if (isRefresh) {
setAllNews(newData);
} else {
setAllNews((prev) => (pageNum === 1 ? newData : [...prev, ...newData]));
}
setHasMore(response?.meta?.hasNextPage ?? false);
setPage(pageNum);
} catch (err) {
console.error("[News] All fetch error:", err);
} finally {
setLoadingAll(false);
setLoadingMore(false);
setRefreshing(false);
}
};
const onRefresh = () => {
setRefreshing(true);
fetchLatest();
fetchAll(1, true);
};
useEffect(() => {
fetchLatest();
fetchAll(1);
}, []);
const loadMore = () => {
if (hasMore && !loadingMore && !loadingAll) {
fetchAll(page + 1);
}
};
const getCategoryColor = (category: string) => {
switch (category) {
case "ANNOUNCEMENT":
return "bg-amber-500";
case "UPDATE":
return "bg-blue-500";
case "MAINTENANCE":
return "bg-red-500";
default:
return "bg-emerald-500";
}
};
const LatestItem = ({ item }: { item: NewsItem }) => (
<Pressable className="mr-4" key={item.id}>
<ShadowWrapper level="md">
<Card
className="overflow-hidden rounded-[20px] bg-card border-border/50"
style={{ width: LATEST_CARD_WIDTH, height: 160 }}
>
<View className="p-5 flex-1 justify-between">
<View>
<View className="flex-row items-center gap-2 mb-2">
<View
className={`px-2 py-0.5 rounded-full ${getCategoryColor(item.category)}`}
>
<Text className="text-[8px] font-black text-white uppercase tracking-tighter">
{item.category}
</Text>
</View>
<Text variant="muted" className="text-[10px] font-bold">
{new Date(item.publishedAt).toLocaleDateString()}
</Text>
</View>
<Text
className="text-foreground font-black text-lg leading-tight"
numberOfLines={2}
>
{item.title}
</Text>
</View>
<View className="flex-row justify-between items-center">
<Text variant="muted" className="text-xs font-medium opacity-60">
Tap to read more
</Text>
<View className="bg-primary/10 p-1.5 rounded-full">
<ChevronRight color="#ea580c" size={14} strokeWidth={3} />
</View>
</View>
</View>
</Card>
</ShadowWrapper>
</Pressable>
);
const NewsItem = ({ item }: { item: NewsItem }) => (
<Pressable className="mb-4" key={item.id}>
<ShadowWrapper level="xs">
<Card className="rounded-[16px] bg-card overflow-hidden border-border/40">
<View className="p-4">
<View className="flex-row items-center gap-2 mb-1.5">
<View
className={`w-1.5 h-1.5 rounded-full ${getCategoryColor(item.category)}`}
/>
<Text
variant="muted"
className="text-[10px] font-black uppercase tracking-widest opacity-60"
>
{item.category}
</Text>
</View>
<Text
className="text-foreground font-bold text-sm mb-1"
numberOfLines={2}
>
{item.title}
</Text>
<Text
variant="muted"
className="text-[11px] leading-relaxed"
numberOfLines={2}
>
{item.content}
</Text>
<View className="flex-row items-center gap-3 mt-3">
<View className="flex-row items-center gap-1">
<Clock color="#94a3b8" size={10} strokeWidth={2.5} />
<Text variant="muted" className="text-[10px] font-medium">
{new Date(item.publishedAt).toLocaleDateString()}
</Text>
</View>
</View>
</View>
</Card>
</ShadowWrapper>
</Pressable>
);
return (
<ScreenWrapper className="bg-background">
<StandardHeader />
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 120 }}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor="#ea580c"
/>
}
>
{/* Latest News Section */}
<View className="px-5 mt-4">
<Text variant="h4" className="text-foreground tracking-tight mb-4">
Latest News
</Text>
{loadingLatest ? (
<ActivityIndicator color="#ea580c" className="py-10" />
) : latestNews.length > 0 ? (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
snapToInterval={LATEST_CARD_WIDTH + 16}
className="overflow-visible"
>
{latestNews.map((item) => (
<LatestItem key={item.id} item={item} />
))}
</ScrollView>
) : (
<View className="bg-card/50 rounded-[12px] p-8 items-center border border-border/50">
<Text variant="muted" className="text-xs font-medium">
No latest items
</Text>
</View>
)}
</View>
{/* All News Section */}
<View className="px-5 mt-8">
<Text variant="h4" className="text-foreground tracking-tight mb-4">
All News
</Text>
{loadingAll ? (
<ActivityIndicator color="#ea580c" className="py-20" />
) : allNews.length > 0 ? (
<>
{allNews.map((item) => (
<NewsItem key={item.id} item={item} />
))}
{hasMore && (
<Pressable
onPress={loadMore}
disabled={loadingMore}
className="py-4 items-center"
>
{loadingMore ? (
<ActivityIndicator color="#ea580c" size="small" />
) : (
<Text className="text-primary font-bold text-xs uppercase tracking-widest">
Load More
</Text>
)}
</Pressable>
)}
</>
) : (
<View className="py-20 items-center">
<Newspaper
color="#94a3b8"
size={48}
strokeWidth={1}
className="mb-4 opacity-20"
/>
<Text
variant="muted"
className="font-bold uppercase tracking-widest text-[10px]"
>
No news items available
</Text>
</View>
)}
</View>
</ScrollView>
</ScreenWrapper>
);
}