467 lines
12 KiB
TypeScript
467 lines
12 KiB
TypeScript
import { View, Text, ScrollView, TouchableOpacity, TextInput, StyleSheet, Platform, Image } from 'react-native'
|
|
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
import { Ionicons } from '@expo/vector-icons'
|
|
import { useRouter } from 'expo-router'
|
|
import { format } from 'date-fns'
|
|
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'
|
|
|
|
// Helper to truncate text
|
|
function getNewsExcerpt(value: string) {
|
|
const normalized = value
|
|
.replace(/[#*_`>-]/g, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
return normalized.length > 85 ? `${normalized.slice(0, 85)}...` : normalized
|
|
}
|
|
|
|
export default function HomeScreen() {
|
|
const router = useRouter()
|
|
const { data: newsItems = [] } = useNewsList()
|
|
const { data: termine = [] } = useTermineListe(true)
|
|
const readIds = useNewsReadStore((s) => s.readIds)
|
|
const { data: me } = trpc.members.me.useQuery()
|
|
const userName = me?.name ?? ''
|
|
|
|
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 QUICK_ACTIONS = [
|
|
{ label: 'Mitglieder', icon: 'people-circle', color: '#2563EB', bg: '#DBEAFE', route: '/(app)/members' },
|
|
{ label: 'Termine', icon: 'alarm', color: '#EA580C', bg: '#FFEDD5', route: '/(app)/termine' },
|
|
{ label: 'Stellen', icon: 'construct', color: '#0F766E', bg: '#CCFBF1', route: '/(app)/stellen' },
|
|
{ label: 'Aktuelles', icon: 'megaphone', color: '#BE185D', bg: '#FCE7F3', route: '/(app)/news' },
|
|
]
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
{/* Decorative Background Element */}
|
|
<View style={styles.bgDecoration} />
|
|
|
|
<SafeAreaView style={styles.safeArea} edges={['top']}>
|
|
<ScrollView
|
|
contentContainerStyle={styles.scrollContent}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
{/* Header Section */}
|
|
<View style={styles.header}>
|
|
<View style={styles.headerLeft}>
|
|
<View style={styles.avatar}>
|
|
<Text style={styles.avatarText}>I</Text>
|
|
</View>
|
|
<View>
|
|
<Text style={styles.greeting}>Willkommen zurück,</Text>
|
|
<Text style={styles.username}>{userName}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<TouchableOpacity
|
|
style={styles.notificationBtn}
|
|
activeOpacity={0.8}
|
|
>
|
|
<Ionicons name="notifications-outline" size={22} color="#1E293B" />
|
|
{unreadCount > 0 && (
|
|
<View style={styles.badge}>
|
|
<Text style={styles.badgeText}>{unreadCount}</Text>
|
|
</View>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Search Bar */}
|
|
<View style={styles.searchContainer}>
|
|
<Ionicons name="search-outline" size={20} color="#94A3B8" />
|
|
<TextInput
|
|
editable={false}
|
|
placeholder="Suchen..."
|
|
placeholderTextColor="#94A3B8"
|
|
style={styles.searchInput}
|
|
/>
|
|
</View>
|
|
|
|
{/* Quick Actions Grid */}
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>Schnellzugriff</Text>
|
|
<View style={styles.grid}>
|
|
{QUICK_ACTIONS.map((action, i) => (
|
|
<TouchableOpacity
|
|
key={i}
|
|
style={styles.gridItem}
|
|
activeOpacity={0.7}
|
|
onPress={() => router.push(action.route as never)}
|
|
>
|
|
<View style={[styles.gridIcon, { backgroundColor: action.bg }]}>
|
|
<Ionicons name={action.icon as any} size={24} color={action.color} />
|
|
</View>
|
|
<Text style={styles.gridLabel}>{action.label}</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
</View>
|
|
|
|
{/* News Section */}
|
|
<View style={styles.section}>
|
|
<View style={styles.sectionHeader}>
|
|
<Text style={styles.sectionTitle}>Aktuelles</Text>
|
|
<TouchableOpacity onPress={() => router.push('/(app)/news' as never)}>
|
|
<Text style={styles.linkText}>Alle anzeigen</Text>
|
|
</TouchableOpacity>
|
|
</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]}
|
|
</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>
|
|
))}
|
|
</View>
|
|
</View>
|
|
|
|
{/* Upcoming Events */}
|
|
<View style={styles.section}>
|
|
<View style={styles.sectionHeader}>
|
|
<Text style={styles.sectionTitle}>Anstehende Termine</Text>
|
|
<TouchableOpacity onPress={() => router.push('/(app)/termine' as never)}>
|
|
<Text style={styles.linkText}>Kalender</Text>
|
|
</TouchableOpacity>
|
|
</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>
|
|
</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>
|
|
|
|
<Ionicons name="chevron-forward" size={16} color="#CBD5E1" />
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
</View>
|
|
|
|
</ScrollView>
|
|
</SafeAreaView>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: '#F8FAFC', // Slate-50
|
|
},
|
|
bgDecoration: {
|
|
position: 'absolute',
|
|
top: -100,
|
|
left: 0,
|
|
right: 0,
|
|
height: 400,
|
|
backgroundColor: '#003B7E', // Primary brand color
|
|
opacity: 0.05,
|
|
transform: [{ scaleX: 1.5 }, { scaleY: 1 }],
|
|
borderBottomLeftRadius: 200,
|
|
borderBottomRightRadius: 200,
|
|
},
|
|
safeArea: {
|
|
flex: 1,
|
|
},
|
|
scrollContent: {
|
|
padding: 20,
|
|
paddingBottom: 40,
|
|
gap: 24,
|
|
},
|
|
|
|
// Header
|
|
header: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginTop: 4,
|
|
},
|
|
headerLeft: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 12,
|
|
},
|
|
avatar: {
|
|
width: 44,
|
|
height: 44,
|
|
borderRadius: 14,
|
|
backgroundColor: '#003B7E',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
shadowColor: '#003B7E',
|
|
shadowOffset: { width: 0, height: 4 },
|
|
shadowOpacity: 0.2,
|
|
shadowRadius: 8,
|
|
elevation: 4,
|
|
},
|
|
avatarText: {
|
|
color: '#FFFFFF',
|
|
fontSize: 20,
|
|
fontWeight: '700',
|
|
},
|
|
greeting: {
|
|
fontSize: 13,
|
|
color: '#64748B',
|
|
fontWeight: '500',
|
|
},
|
|
username: {
|
|
fontSize: 18,
|
|
fontWeight: '700',
|
|
color: '#0F172A',
|
|
},
|
|
notificationBtn: {
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: 20,
|
|
backgroundColor: '#FFFFFF',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
borderWidth: 1,
|
|
borderColor: '#E2E8F0',
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.05,
|
|
shadowRadius: 4,
|
|
elevation: 1,
|
|
},
|
|
badge: {
|
|
position: 'absolute',
|
|
top: -2,
|
|
right: -2,
|
|
backgroundColor: '#EF4444',
|
|
width: 16,
|
|
height: 16,
|
|
borderRadius: 8,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
borderWidth: 1.5,
|
|
borderColor: '#FFFFFF',
|
|
},
|
|
badgeText: {
|
|
color: '#FFF',
|
|
fontSize: 9,
|
|
fontWeight: 'bold',
|
|
},
|
|
|
|
// Search
|
|
searchContainer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: '#FFFFFF',
|
|
borderWidth: 1,
|
|
borderColor: '#E2E8F0',
|
|
borderRadius: 16,
|
|
paddingHorizontal: 14,
|
|
paddingVertical: 12,
|
|
gap: 10,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.03,
|
|
shadowRadius: 4,
|
|
elevation: 1,
|
|
},
|
|
searchInput: {
|
|
flex: 1,
|
|
fontSize: 15,
|
|
color: '#0F172A',
|
|
},
|
|
|
|
// Sections
|
|
section: {
|
|
gap: 12,
|
|
},
|
|
sectionHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
},
|
|
sectionTitle: {
|
|
fontSize: 18,
|
|
fontWeight: '700',
|
|
color: '#0F172A',
|
|
},
|
|
linkText: {
|
|
fontSize: 14,
|
|
fontWeight: '600',
|
|
color: '#003B7E',
|
|
},
|
|
|
|
// Grid
|
|
grid: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
gap: 12,
|
|
},
|
|
gridItem: {
|
|
width: '48%', // Approx half with gap
|
|
backgroundColor: '#FFFFFF',
|
|
padding: 16,
|
|
borderRadius: 20,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: 10,
|
|
borderWidth: 1,
|
|
borderColor: '#E2E8F0',
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.02,
|
|
shadowRadius: 8,
|
|
elevation: 2,
|
|
},
|
|
gridIcon: {
|
|
width: 48,
|
|
height: 48,
|
|
borderRadius: 14,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
gridLabel: {
|
|
fontSize: 14,
|
|
fontWeight: '600',
|
|
color: '#334155',
|
|
},
|
|
|
|
// News Cards
|
|
cardsColumn: {
|
|
gap: 12,
|
|
},
|
|
newsCard: {
|
|
backgroundColor: '#FFFFFF',
|
|
padding: 16,
|
|
borderRadius: 18,
|
|
borderWidth: 1,
|
|
borderColor: '#E2E8F0',
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.04,
|
|
shadowRadius: 6,
|
|
elevation: 2,
|
|
},
|
|
newsHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: 8,
|
|
},
|
|
categoryBadge: {
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 4,
|
|
backgroundColor: '#F1F5F9',
|
|
borderRadius: 8,
|
|
},
|
|
categoryText: {
|
|
fontSize: 10,
|
|
fontWeight: '700',
|
|
color: '#475569',
|
|
textTransform: 'uppercase',
|
|
},
|
|
dateText: {
|
|
fontSize: 12,
|
|
color: '#94A3B8',
|
|
},
|
|
newsTitle: {
|
|
fontSize: 16,
|
|
fontWeight: '700',
|
|
color: '#0F172A',
|
|
marginBottom: 6,
|
|
lineHeight: 22,
|
|
},
|
|
newsBody: {
|
|
fontSize: 14,
|
|
color: '#64748B',
|
|
lineHeight: 20,
|
|
},
|
|
|
|
// Events List
|
|
eventsList: {
|
|
backgroundColor: '#FFFFFF',
|
|
borderRadius: 20,
|
|
borderWidth: 1,
|
|
borderColor: '#E2E8F0',
|
|
paddingVertical: 4,
|
|
},
|
|
eventRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
padding: 12,
|
|
gap: 14,
|
|
},
|
|
eventBorder: {
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: '#F1F5F9',
|
|
},
|
|
dateBox: {
|
|
width: 50,
|
|
height: 50,
|
|
borderRadius: 14,
|
|
backgroundColor: '#F8FAFC',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
borderWidth: 1,
|
|
borderColor: '#E2E8F0',
|
|
},
|
|
dateMonth: {
|
|
fontSize: 10,
|
|
fontWeight: '700',
|
|
textTransform: 'uppercase',
|
|
color: '#64748B',
|
|
marginBottom: -2,
|
|
},
|
|
dateDay: {
|
|
fontSize: 18,
|
|
fontWeight: '800',
|
|
color: '#0F172A',
|
|
},
|
|
eventInfo: {
|
|
flex: 1,
|
|
gap: 2,
|
|
},
|
|
eventTitle: {
|
|
fontSize: 15,
|
|
fontWeight: '700',
|
|
color: '#0F172A',
|
|
},
|
|
eventMeta: {
|
|
fontSize: 12,
|
|
color: '#64748B',
|
|
fontWeight: '500',
|
|
},
|
|
})
|