push
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { View, Text, ScrollView, TouchableOpacity, TextInput, StyleSheet, Platform, Image } from 'react-native'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { View, Text, ScrollView, TouchableOpacity, TextInput, StyleSheet, Platform, Image, Animated } from 'react-native'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { Ionicons } from '@expo/vector-icons'
|
||||
import { useRouter } from 'expo-router'
|
||||
@@ -7,8 +8,8 @@ import { de } from 'date-fns/locale'
|
||||
import { NEWS_KATEGORIE_LABELS } from '@innungsapp/shared/types'
|
||||
import { useNewsList } from '@/hooks/useNews'
|
||||
import { useTermineListe } from '@/hooks/useTermine'
|
||||
import { useNewsReadStore } from '@/store/news.store'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import { getOrgSlug } from '@/lib/org-config'
|
||||
|
||||
// Helper to truncate text
|
||||
function getNewsExcerpt(value: string) {
|
||||
@@ -19,17 +20,45 @@ function getNewsExcerpt(value: string) {
|
||||
return normalized.length > 85 ? `${normalized.slice(0, 85)}...` : normalized
|
||||
}
|
||||
|
||||
const AVATAR_PALETTES = [
|
||||
{ bg: '#003B7E' }, { bg: '#1D4ED8' }, { bg: '#059669' },
|
||||
{ bg: '#4338CA' }, { bg: '#B45309' }, { bg: '#0F766E' },
|
||||
]
|
||||
|
||||
function Skeleton({ style }: { style?: any }) {
|
||||
const anim = useRef(new Animated.Value(0.4)).current
|
||||
|
||||
useEffect(() => {
|
||||
Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(anim, { toValue: 1, duration: 1000, useNativeDriver: true }),
|
||||
Animated.timing(anim, { toValue: 0.4, duration: 1000, useNativeDriver: true }),
|
||||
])
|
||||
).start()
|
||||
}, [anim])
|
||||
|
||||
return <Animated.View style={[{ backgroundColor: '#E2E8F0', borderRadius: 8 }, style, { opacity: anim }]} />
|
||||
}
|
||||
function getInitials(name: string) {
|
||||
return name.split(' ').slice(0, 2).map((w) => w[0]?.toUpperCase() ?? '').join('')
|
||||
}
|
||||
function getAvatarBg(name: string) {
|
||||
return AVATAR_PALETTES[name.charCodeAt(0) % AVATAR_PALETTES.length].bg
|
||||
}
|
||||
|
||||
export default function HomeScreen() {
|
||||
const router = useRouter()
|
||||
const { data: newsItems = [] } = useNewsList()
|
||||
const { data: termine = [] } = useTermineListe(true)
|
||||
const readIds = useNewsReadStore((s) => s.readIds)
|
||||
const { data: newsItems = [], isLoading: newsLoading } = useNewsList()
|
||||
const { data: termine = [], isLoading: termineLoading } = useTermineListe(true)
|
||||
const { data: me } = trpc.members.me.useQuery()
|
||||
const { data: unread } = trpc.messages.unreadCount.useQuery(undefined, { refetchInterval: 15_000 })
|
||||
const { data: org } = trpc.organizations.bySlug.useQuery({ slug: getOrgSlug() })
|
||||
const userName = me?.name ?? ''
|
||||
const orgName = org?.name ?? 'InnungsApp'
|
||||
|
||||
const latestNews = newsItems.slice(0, 2)
|
||||
const upcomingEvents = termine.slice(0, 3)
|
||||
const unreadCount = newsItems.filter((item) => !(item.isRead || readIds.has(item.id))).length
|
||||
const unreadCount = unread?.count ?? 0
|
||||
|
||||
const QUICK_ACTIONS = [
|
||||
{ label: 'Mitglieder', icon: 'people-circle', color: '#2563EB', bg: '#DBEAFE', route: '/(app)/members' },
|
||||
@@ -51,11 +80,16 @@ export default function HomeScreen() {
|
||||
{/* Header Section */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
<View style={styles.avatar}>
|
||||
<Text style={styles.avatarText}>I</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push('/(app)/profil' as never)}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<View style={[styles.avatar, { backgroundColor: userName ? getAvatarBg(userName) : '#003B7E' }]}>
|
||||
<Text style={styles.avatarText}>{userName ? getInitials(userName) : '?'}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View>
|
||||
<Text style={styles.greeting}>Willkommen zurück,</Text>
|
||||
<Text style={styles.greeting}>{orgName}</Text>
|
||||
<Text style={styles.username}>{userName}</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -63,8 +97,9 @@ export default function HomeScreen() {
|
||||
<TouchableOpacity
|
||||
style={styles.notificationBtn}
|
||||
activeOpacity={0.8}
|
||||
onPress={() => router.push('/(app)/chat' as never)}
|
||||
>
|
||||
<Ionicons name="notifications-outline" size={22} color="#1E293B" />
|
||||
<Ionicons name="chatbubbles-outline" size={22} color="#1E293B" />
|
||||
{unreadCount > 0 && (
|
||||
<View style={styles.badge}>
|
||||
<Text style={styles.badgeText}>{unreadCount}</Text>
|
||||
@@ -114,28 +149,37 @@ export default function HomeScreen() {
|
||||
</View>
|
||||
|
||||
<View style={styles.cardsColumn}>
|
||||
{latestNews.map((item) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={styles.newsCard}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => router.push(`/(app)/news/${item.id}` as never)}
|
||||
>
|
||||
<View style={styles.newsHeader}>
|
||||
<View style={styles.categoryBadge}>
|
||||
<Text style={styles.categoryText}>
|
||||
{NEWS_KATEGORIE_LABELS[item.kategorie]}
|
||||
{newsLoading ? (
|
||||
<>
|
||||
<Skeleton style={[styles.newsCard, { height: 130 }]} />
|
||||
<Skeleton style={[styles.newsCard, { height: 130 }]} />
|
||||
</>
|
||||
) : latestNews.length > 0 ? (
|
||||
latestNews.map((item) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={styles.newsCard}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => router.push(`/(app)/news/${item.id}` as never)}
|
||||
>
|
||||
<View style={styles.newsHeader}>
|
||||
<View style={styles.categoryBadge}>
|
||||
<Text style={styles.categoryText}>
|
||||
{NEWS_KATEGORIE_LABELS[item.kategorie]}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.dateText}>
|
||||
{item.publishedAt ? format(item.publishedAt, 'dd. MMM', { locale: de }) : 'Entwurf'}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.dateText}>
|
||||
{item.publishedAt ? format(item.publishedAt, 'dd. MMM', { locale: de }) : 'Entwurf'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text style={styles.newsTitle} numberOfLines={2}>{item.title}</Text>
|
||||
<Text style={styles.newsBody} numberOfLines={2}>{getNewsExcerpt(item.body)}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
<Text style={styles.newsTitle} numberOfLines={2}>{item.title}</Text>
|
||||
<Text style={styles.newsBody} numberOfLines={2}>{getNewsExcerpt(item.body)}</Text>
|
||||
</TouchableOpacity>
|
||||
))
|
||||
) : (
|
||||
<Text style={styles.emptyText}>Keine aktuellen Neuigkeiten</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -149,28 +193,58 @@ export default function HomeScreen() {
|
||||
</View>
|
||||
|
||||
<View style={styles.eventsList}>
|
||||
{upcomingEvents.map((event, index) => (
|
||||
<TouchableOpacity
|
||||
key={event.id}
|
||||
style={[styles.eventRow, index !== upcomingEvents.length - 1 && styles.eventBorder]}
|
||||
onPress={() => router.push(`/(app)/termine/${event.id}` as never)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.dateBox}>
|
||||
<Text style={styles.dateMonth}>{format(event.datum, 'MMM', { locale: de })}</Text>
|
||||
<Text style={styles.dateDay}>{format(event.datum, 'dd')}</Text>
|
||||
{termineLoading ? (
|
||||
<>
|
||||
<View style={[styles.eventRow, styles.eventBorder]}>
|
||||
<Skeleton style={styles.dateBox} />
|
||||
<View style={styles.eventInfo}>
|
||||
<Skeleton style={{ width: '70%', height: 16, marginBottom: 6 }} />
|
||||
<Skeleton style={{ width: '40%', height: 12 }} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.eventInfo}>
|
||||
<Text style={styles.eventTitle} numberOfLines={1}>{event.titel}</Text>
|
||||
<Text style={styles.eventMeta} numberOfLines={1}>
|
||||
{event.uhrzeit} • {event.ort}
|
||||
</Text>
|
||||
<View style={[styles.eventRow, styles.eventBorder]}>
|
||||
<Skeleton style={styles.dateBox} />
|
||||
<View style={styles.eventInfo}>
|
||||
<Skeleton style={{ width: '60%', height: 16, marginBottom: 6 }} />
|
||||
<Skeleton style={{ width: '50%', height: 12 }} />
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.eventRow}>
|
||||
<Skeleton style={styles.dateBox} />
|
||||
<View style={styles.eventInfo}>
|
||||
<Skeleton style={{ width: '80%', height: 16, marginBottom: 6 }} />
|
||||
<Skeleton style={{ width: '30%', height: 12 }} />
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
) : upcomingEvents.length > 0 ? (
|
||||
upcomingEvents.map((event, index) => (
|
||||
<TouchableOpacity
|
||||
key={event.id}
|
||||
style={[styles.eventRow, index !== upcomingEvents.length - 1 && styles.eventBorder]}
|
||||
onPress={() => router.push(`/(app)/termine/${event.id}` as never)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.dateBox}>
|
||||
<Text style={styles.dateMonth}>{format(event.datum, 'MMM', { locale: de })}</Text>
|
||||
<Text style={styles.dateDay}>{format(event.datum, 'dd')}</Text>
|
||||
</View>
|
||||
|
||||
<Ionicons name="chevron-forward" size={16} color="#CBD5E1" />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
<View style={styles.eventInfo}>
|
||||
<Text style={styles.eventTitle} numberOfLines={1}>{event.titel}</Text>
|
||||
<Text style={styles.eventMeta} numberOfLines={1}>
|
||||
{event.uhrzeit} • {event.ort}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Ionicons name="chevron-forward" size={16} color="#CBD5E1" />
|
||||
</TouchableOpacity>
|
||||
))
|
||||
) : (
|
||||
<View style={{ padding: 16 }}>
|
||||
<Text style={styles.emptyText}>Keine anstehenden Termine</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -221,20 +295,21 @@ const styles = StyleSheet.create({
|
||||
avatar: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 14,
|
||||
backgroundColor: '#003B7E',
|
||||
borderRadius: 22,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
shadowColor: '#003B7E',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 6,
|
||||
elevation: 3,
|
||||
},
|
||||
avatarText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
fontSize: 16,
|
||||
fontWeight: '800',
|
||||
lineHeight: 20,
|
||||
includeFontPadding: false,
|
||||
},
|
||||
greeting: {
|
||||
fontSize: 13,
|
||||
@@ -463,4 +538,10 @@ const styles = StyleSheet.create({
|
||||
color: '#64748B',
|
||||
fontWeight: '500',
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 14,
|
||||
color: '#94A3B8',
|
||||
textAlign: 'center',
|
||||
paddingVertical: 12,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user