feat: Implement mobile application and lead processing utilities.
This commit is contained in:
464
innungsapp/apps/mobile/app/(app)/home/index.tsx
Normal file
464
innungsapp/apps/mobile/app/(app)/home/index.tsx
Normal file
@@ -0,0 +1,464 @@
|
||||
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'
|
||||
|
||||
// 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 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', color: '#003B7E', bg: '#E0F2FE', route: '/(app)/members' },
|
||||
{ label: 'Termine', icon: 'calendar', color: '#B45309', bg: '#FEF3C7', route: '/(app)/termine' },
|
||||
{ label: 'Stellen', icon: 'briefcase', color: '#059669', bg: '#D1FAE5', route: '/(app)/stellen' },
|
||||
{ label: 'Profil', icon: 'person', color: '#4F46E5', bg: '#E0E7FF', route: '/(app)/profil' },
|
||||
]
|
||||
|
||||
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}>Demo Admin</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',
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user