stadtwerke/innungsapp/apps/mobile/components/termine/TerminCard.tsx

254 lines
6.1 KiB
TypeScript

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,
},
})