feat: Implement mobile application and lead processing utilities.
This commit is contained in:
253
innungsapp/apps/mobile/components/termine/TerminCard.tsx
Normal file
253
innungsapp/apps/mobile/components/termine/TerminCard.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import { View, Text, TouchableOpacity, ActivityIndicator, StyleSheet } from 'react-native'
|
||||
import { Ionicons } from '@expo/vector-icons'
|
||||
import { Badge } from '@/components/ui/Badge'
|
||||
import { TERMIN_TYP_LABELS } from '@innungsapp/shared/types'
|
||||
import { format } from 'date-fns'
|
||||
import { de } from 'date-fns/locale'
|
||||
|
||||
interface TerminCardProps {
|
||||
termin: {
|
||||
id: string
|
||||
titel: string
|
||||
datum: Date
|
||||
uhrzeit: string | null
|
||||
ort: string | null
|
||||
typ: string
|
||||
isAngemeldet: boolean
|
||||
teilnehmerAnzahl: number
|
||||
}
|
||||
onPress: () => void
|
||||
onToggleAnmeldung?: () => void
|
||||
isToggling?: boolean
|
||||
isPast?: boolean
|
||||
}
|
||||
|
||||
export function TerminCard({
|
||||
termin,
|
||||
onPress,
|
||||
onToggleAnmeldung,
|
||||
isToggling = false,
|
||||
isPast = false,
|
||||
}: TerminCardProps) {
|
||||
const datum = new Date(termin.datum)
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
style={[styles.card, isPast && styles.cardPast]}
|
||||
activeOpacity={0.76}
|
||||
>
|
||||
{/* Date column */}
|
||||
<View style={[styles.dateColumn, isPast && styles.dateColumnPast]}>
|
||||
<Text style={[styles.dayNumber, isPast && styles.dayNumberPast]}>
|
||||
{format(datum, 'dd')}
|
||||
</Text>
|
||||
<Text style={[styles.monthLabel, isPast && styles.monthLabelPast]}>
|
||||
{format(datum, 'MMM', { locale: de }).toUpperCase()}
|
||||
</Text>
|
||||
{!isPast && termin.isAngemeldet && <View style={styles.registeredDot} />}
|
||||
{isPast && (
|
||||
<View style={styles.pastBadge}>
|
||||
<Text style={styles.pastBadgeText}>vorbei</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Divider */}
|
||||
<View style={[styles.divider, isPast && styles.dividerPast]} />
|
||||
|
||||
{/* Content */}
|
||||
<View style={styles.content}>
|
||||
<Badge label={TERMIN_TYP_LABELS[termin.typ]} typ={termin.typ} />
|
||||
<Text style={[styles.title, isPast && styles.titlePast]} numberOfLines={2}>
|
||||
{termin.titel}
|
||||
</Text>
|
||||
<View style={styles.metaRow}>
|
||||
{termin.uhrzeit && (
|
||||
<View style={styles.metaItem}>
|
||||
<Ionicons name="time-outline" size={12} color={isPast ? '#94A3B8' : '#64748B'} />
|
||||
<Text style={[styles.metaText, isPast && styles.metaTextPast]}>
|
||||
{termin.uhrzeit} Uhr
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{termin.ort && (
|
||||
<View style={styles.metaItem}>
|
||||
<Ionicons name="location-outline" size={12} color={isPast ? '#94A3B8' : '#64748B'} />
|
||||
<Text style={[styles.metaText, isPast && styles.metaTextPast]} numberOfLines={1}>
|
||||
{termin.ort}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Registration status chip — only for upcoming events */}
|
||||
{!isPast && termin.isAngemeldet && onToggleAnmeldung && (
|
||||
<TouchableOpacity
|
||||
onPress={(e) => {
|
||||
e.stopPropagation?.()
|
||||
onToggleAnmeldung()
|
||||
}}
|
||||
style={styles.registeredChip}
|
||||
disabled={isToggling}
|
||||
activeOpacity={0.75}
|
||||
>
|
||||
{isToggling ? (
|
||||
<ActivityIndicator size="small" color="#047857" style={{ marginRight: 4 }} />
|
||||
) : (
|
||||
<Ionicons name="checkmark-circle" size={14} color="#059669" />
|
||||
)}
|
||||
<Text style={styles.registeredChipText}>
|
||||
Angemeldet · <Text style={styles.abmeldenText}>Abmelden</Text>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Chevron */}
|
||||
<View style={styles.chevronWrap}>
|
||||
<Ionicons name="chevron-forward" size={20} color={isPast ? '#E2E8F0' : '#CBD5E1'} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
shadowColor: '#1C1917',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 12,
|
||||
elevation: 3,
|
||||
},
|
||||
cardPast: {
|
||||
shadowOpacity: 0.04,
|
||||
elevation: 1,
|
||||
},
|
||||
dateColumn: {
|
||||
width: 64,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 18,
|
||||
backgroundColor: '#EFF6FF',
|
||||
},
|
||||
dateColumnPast: {
|
||||
backgroundColor: '#F4F4F5',
|
||||
},
|
||||
dayNumber: {
|
||||
fontSize: 28,
|
||||
fontWeight: '800',
|
||||
color: '#003B7E',
|
||||
lineHeight: 30,
|
||||
},
|
||||
dayNumberPast: {
|
||||
color: '#94A3B8',
|
||||
},
|
||||
monthLabel: {
|
||||
fontSize: 10,
|
||||
fontWeight: '700',
|
||||
color: '#003B7E',
|
||||
letterSpacing: 1,
|
||||
opacity: 0.75,
|
||||
marginTop: 1,
|
||||
},
|
||||
monthLabelPast: {
|
||||
color: '#94A3B8',
|
||||
opacity: 1,
|
||||
},
|
||||
registeredDot: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: '#059669',
|
||||
marginTop: 6,
|
||||
},
|
||||
pastBadge: {
|
||||
marginTop: 6,
|
||||
backgroundColor: '#E2E8F0',
|
||||
borderRadius: 99,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
},
|
||||
pastBadgeText: {
|
||||
fontSize: 9,
|
||||
fontWeight: '700',
|
||||
color: '#94A3B8',
|
||||
letterSpacing: 0.5,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
divider: {
|
||||
width: 1,
|
||||
alignSelf: 'stretch',
|
||||
marginVertical: 14,
|
||||
backgroundColor: '#F0EDED',
|
||||
},
|
||||
dividerPast: {
|
||||
backgroundColor: '#F4F4F5',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 14,
|
||||
gap: 8,
|
||||
},
|
||||
title: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
color: '#0F172A',
|
||||
letterSpacing: -0.2,
|
||||
lineHeight: 21,
|
||||
marginTop: 2,
|
||||
},
|
||||
titlePast: {
|
||||
color: '#94A3B8',
|
||||
fontWeight: '500',
|
||||
},
|
||||
metaRow: {
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
marginTop: 4,
|
||||
},
|
||||
metaItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
metaText: {
|
||||
fontSize: 12,
|
||||
color: '#64748B',
|
||||
},
|
||||
metaTextPast: {
|
||||
color: '#94A3B8',
|
||||
},
|
||||
registeredChip: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
marginTop: 4,
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: '#ECFDF5',
|
||||
borderRadius: 99,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 5,
|
||||
borderWidth: 1,
|
||||
borderColor: '#A7F3D0',
|
||||
},
|
||||
registeredChipText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: '#047857',
|
||||
},
|
||||
abmeldenText: {
|
||||
color: '#DC2626',
|
||||
fontWeight: '600',
|
||||
},
|
||||
chevronWrap: {
|
||||
paddingRight: 12,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user