diff --git a/innungsapp/apps/mobile/app/(app)/news/[id].tsx b/innungsapp/apps/mobile/app/(app)/news/[id].tsx index 819042c..95fb99a 100644 --- a/innungsapp/apps/mobile/app/(app)/news/[id].tsx +++ b/innungsapp/apps/mobile/app/(app)/news/[id].tsx @@ -7,6 +7,7 @@ import { useLocalSearchParams, useRouter } from 'expo-router' import { useEffect } from 'react' import { Ionicons } from '@expo/vector-icons' import { useNewsDetail } from '@/hooks/useNews' +import { useNewsReadStore } from '@/store/news.store' import { AttachmentRow } from '@/components/news/AttachmentRow' import { Badge } from '@/components/ui/Badge' import { NEWS_KATEGORIE_LABELS } from '@innungsapp/shared/types' @@ -98,8 +99,14 @@ export default function NewsDetailScreen() { const { id } = useLocalSearchParams<{ id: string }>() const router = useRouter() const { data: news, isLoading, onOpen } = useNewsDetail(id) + const markRead = useNewsReadStore((s) => s.markRead) - useEffect(() => { if (news) onOpen() }, [news?.id]) + useEffect(() => { + if (news) { + markRead(news.id) // sofort lokal markieren → Badge weg beim Zurückgehen + onOpen() + } + }, [news?.id]) if (isLoading) { return ( @@ -121,7 +128,7 @@ export default function NewsDetailScreen() { {/* Nav bar */} router.back()} + onPress={() => router.navigate('/(app)/news')} style={styles.backBtn} activeOpacity={0.7} > diff --git a/innungsapp/apps/mobile/app/(app)/news/index.tsx b/innungsapp/apps/mobile/app/(app)/news/index.tsx index f0ae26c..f2268f8 100644 --- a/innungsapp/apps/mobile/app/(app)/news/index.tsx +++ b/innungsapp/apps/mobile/app/(app)/news/index.tsx @@ -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(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 ( Aktuelles - {unreadCount > 0 && ( - - {unreadCount} neu - - )} {FILTERS.map((opt) => { const active = kategorie === opt.value + const count = getUnreadCount(opt.value) return ( {opt.label} + {count > 0 && ( + + + {count} + + + )} ) })} @@ -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', diff --git a/innungsapp/apps/mobile/components/news/NewsCard.tsx b/innungsapp/apps/mobile/components/news/NewsCard.tsx index 8fe847f..d6b7b23 100644 --- a/innungsapp/apps/mobile/components/news/NewsCard.tsx +++ b/innungsapp/apps/mobile/components/news/NewsCard.tsx @@ -24,22 +24,22 @@ export function NewsCard({ news, onPress }: NewsCardProps) { return ( - {!isRead && ( - - Neu - - )} - - + + + {!isRead && ( + + Neu + + )} + {news.publishedAt ? format(new Date(news.publishedAt), 'dd. MMM', { locale: de }) : 'Entwurf'} - {news.title} @@ -71,15 +71,16 @@ const styles = StyleSheet.create({ elevation: 2, position: 'relative', }, + badgeRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + }, newBadge: { - position: 'absolute', - right: 30, - top: 8, borderRadius: 999, backgroundColor: '#EF4444', - paddingHorizontal: 8, + paddingHorizontal: 7, paddingVertical: 2, - zIndex: 2, }, newBadgeText: { color: '#FFFFFF', diff --git a/innungsapp/packages/shared/prisma/dev.db b/innungsapp/packages/shared/prisma/dev.db index 3e66ff8..62c4ae4 100644 Binary files a/innungsapp/packages/shared/prisma/dev.db and b/innungsapp/packages/shared/prisma/dev.db differ