feat: Implement news section with list and detail views, category filtering, and unread indicators.

This commit is contained in:
Timo Knuth
2026-02-27 19:36:20 +01:00
parent 4863d032d9
commit 244da5e69a
4 changed files with 66 additions and 32 deletions

View File

@@ -9,6 +9,7 @@ import { useNewsList } from '@/hooks/useNews'
import { NewsCard } from '@/components/news/NewsCard'
import { EmptyState } from '@/components/ui/EmptyState'
import { LoadingSpinner } from '@/components/ui/LoadingSpinner'
import { useNewsReadStore } from '@/store/news.store'
function SkeletonCard() {
const anim = useRef(new Animated.Value(0.4)).current
@@ -50,7 +51,10 @@ export default function NewsScreen() {
const router = useRouter()
const [kategorie, setKategorie] = useState<string | undefined>(undefined)
const [showSkeleton, setShowSkeleton] = useState(true)
// Fetch all news (without category filter) to compute per-category unread counts
const { data: allData, refetch: refetchAll, isRefetching: isRefetchingAll } = useNewsList(undefined)
const { data, isLoading, refetch, isRefetching } = useNewsList(kategorie)
const localReadIds = useNewsReadStore((s) => s.readIds)
useFocusEffect(
useCallback(() => {
@@ -60,18 +64,20 @@ export default function NewsScreen() {
}, [])
)
const unreadCount = data?.filter((n) => !n.isRead).length ?? 0
// Compute unread count per category (combining server isRead + local store)
function getUnreadCount(filterValue: string | undefined) {
const source = allData ?? []
const filtered = filterValue === undefined
? source
: source.filter((n) => n.kategorie === filterValue)
return filtered.filter((n) => !n.isRead && !localReadIds.has(n.id)).length
}
return (
<SafeAreaView style={styles.safeArea} edges={['top']}>
<View style={styles.header}>
<View style={styles.titleRow}>
<Text style={styles.screenTitle}>Aktuelles</Text>
{unreadCount > 0 && (
<View style={styles.unreadBadge}>
<Text style={styles.unreadBadgeText}>{unreadCount} neu</Text>
</View>
)}
</View>
<ScrollView
@@ -81,6 +87,7 @@ export default function NewsScreen() {
>
{FILTERS.map((opt) => {
const active = kategorie === opt.value
const count = getUnreadCount(opt.value)
return (
<TouchableOpacity
key={String(opt.value)}
@@ -91,6 +98,13 @@ export default function NewsScreen() {
<Text style={[styles.chipLabel, active && styles.chipLabelActive]}>
{opt.label}
</Text>
{count > 0 && (
<View style={[styles.chipBadge, active && styles.chipBadgeActive]}>
<Text style={[styles.chipBadgeText, active && styles.chipBadgeTextActive]}>
{count}
</Text>
</View>
)}
</TouchableOpacity>
)
})}
@@ -152,23 +166,15 @@ const styles = StyleSheet.create({
color: '#0F172A',
letterSpacing: -0.5,
},
unreadBadge: {
backgroundColor: '#EFF6FF',
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 99,
},
unreadBadgeText: {
color: '#003B7E',
fontSize: 12,
fontWeight: '700',
},
filterScroll: {
paddingBottom: 14,
gap: 8,
paddingRight: 20,
},
chip: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
paddingHorizontal: 14,
paddingVertical: 7,
borderRadius: 99,
@@ -188,6 +194,26 @@ const styles = StyleSheet.create({
chipLabelActive: {
color: '#FFFFFF',
},
chipBadge: {
backgroundColor: '#003B7E',
borderRadius: 99,
minWidth: 18,
height: 18,
paddingHorizontal: 5,
alignItems: 'center',
justifyContent: 'center',
},
chipBadgeActive: {
backgroundColor: '#FFFFFF',
},
chipBadgeText: {
fontSize: 10,
fontWeight: '700',
color: '#FFFFFF',
},
chipBadgeTextActive: {
color: '#003B7E',
},
divider: {
height: 1,
backgroundColor: '#E2E8F0',