Files
time_to_leave/apps/mobile/src/screens/EventListScreen.tsx
T

209 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useState } from 'react';
import {
FlatList,
RefreshControl,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { useFocusEffect } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { RouteProp } from '@react-navigation/native';
import { loadEvents, removeEvent } from '../store/eventStore';
import { calculateCountdown } from '@timetoleave/core';
import type { Event as CalendarEvent } from '@timetoleave/core';
import type { RootStack } from '../types/navigation';
import { useColors } from '../hooks/useColors';
type ScreenProps = {
navigation: NativeStackNavigationProp<RootStack, 'EventList'>;
route: RouteProp<RootStack, 'EventList'>;
};
/**
* Home screen showing a scrollable list of upcoming events with countdown
* badges. Pull-to-refresh reloads events from storage. Reloads automatically
* when the screen gains focus (so edits on other screens are reflected).
*/
export function EventListScreen({ navigation }: ScreenProps) {
const colors = useColors();
const [events, setEvents] = useState<CalendarEvent[]>([]);
const [refreshing, setRefreshing] = useState(false);
// Force countdown recalculation periodically
const [, setTick] = useState(0);
const reload = useCallback(async () => {
const list = await loadEvents();
setEvents(list);
}, []);
useEffect(() => { reload(); }, [reload]);
// Recalculate countdowns every 30 seconds
useEffect(() => {
const interval = setInterval(() => setTick(t => t + 1), 30_000);
return () => clearInterval(interval);
}, []);
useFocusEffect(
useCallback(() => { reload(); }, [reload]),
);
const onRefresh = async () => {
setRefreshing(true);
await reload();
setRefreshing(false);
};
const renderItem = ({ item }: { item: CalendarEvent }) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const countdown = calculateCountdown(item.eventTime);
// Derive a simple status — journeys aren't loaded on the list screen for MVP
// so we show countdown-based status instead
const status = countdown.urgent ? 'Bald!' : countdown.label;
return (
<View style={styles.cardWrapper}>
<TouchableOpacity
onPress={() => navigation.navigate('EventDetail', { eventId: item.id })}
activeOpacity={0.6}
style={{ flex: 1 }}
>
<View style={[styles.card, { backgroundColor: colors.card }]}>
<View style={styles.dotRow}>
<View style={[styles.dot, { backgroundColor: countdown.urgent ? colors.delete : '#34C759' }]} />
<Text style={[styles.title, { color: colors.text }]}>{item.title}</Text>
<Text style={[styles.badge, { color: countdown.urgent ? colors.delete : colors.accent }]}>
{countdown.label}
</Text>
</View>
<Text style={[styles.subtitle, { color: colors.subtext }]}>{item.destination}</Text>
<Text style={[styles.time, { color: colors.accent }]}>
{item.eventTime.toLocaleString('de-AT', {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit',
})}
</Text>
<Text style={[styles.status, { color: countdown.urgent ? colors.delete : '#34C759' }]}>{status}</Text>
</View>
</TouchableOpacity>
{/* Edit button */}
<TouchableOpacity
onPress={() => navigation.navigate('AddEvent', { editEventId: item.id })}
style={styles.editBtn}
>
<Text style={[styles.editText, { color: colors.accent }]}> Bearbeiten</Text>
</TouchableOpacity>
{/* Delete button */}
<TouchableOpacity onPress={() => removeEvent(item.id, reload)} style={styles.deleteBtn}>
<Text style={[styles.deleteText, { color: colors.delete }]}>Entfernen</Text>
</TouchableOpacity>
</View>
);
};
if (events.length === 0) {
return (
<View style={[styles.center, { backgroundColor: colors.background }]}>
<Text style={[styles.empty, { color: colors.subtext }]}>Keine Termine</Text>
<TouchableOpacity
style={styles.addBtn}
onPress={() => navigation.navigate('AddEvent')}
>
<Text style={styles.addBtnText}>+ Termin hinzufügen</Text>
</TouchableOpacity>
</View>
);
}
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<View style={styles.topBar}>
<TouchableOpacity
onPress={() => navigation.navigate('CalendarImport')}
style={styles.topBtn}
>
<Text style={[styles.topBtnText, { color: colors.accent }]}>📅 Kalender</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => navigation.navigate('Settings')}
style={styles.topBtn}
>
<Text style={[styles.topBtnText, { color: colors.accent }]}> Einstellungen</Text>
</TouchableOpacity>
</View>
<FlatList
data={events}
keyExtractor={(item) => item.id}
renderItem={renderItem}
contentContainerStyle={styles.list}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={colors.accent} />
}
/>
<TouchableOpacity
style={styles.fab}
onPress={() => navigation.navigate('AddEvent')}
>
<Text style={styles.fabText}>+</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
topBar: { flexDirection: 'row', justifyContent: 'flex-end', padding: 8, gap: 8 },
topBtn: { paddingHorizontal: 12, paddingVertical: 6 },
topBtnText: { fontSize: 15 },
list: { padding: 12 },
cardWrapper: { marginBottom: 12 },
card: {
borderRadius: 12,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
elevation: 2,
},
dotRow: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 4 },
dot: { width: 10, height: 10, borderRadius: 5 },
title: { fontSize: 18, fontWeight: '600', flex: 1 },
badge: { fontSize: 12, fontWeight: '600' },
subtitle: { fontSize: 14, marginBottom: 4 },
time: { fontSize: 13 },
status: { fontSize: 13, marginTop: 2, fontWeight: '500' },
editBtn: { alignSelf: 'flex-start', marginTop: 4 },
editText: { fontSize: 13, fontWeight: '500' },
deleteBtn: { alignSelf: 'flex-start', marginTop: 2, marginBottom: 4 },
deleteText: { fontSize: 13 },
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
empty: { fontSize: 20, marginBottom: 16 },
addBtn: { backgroundColor: '#8B5CF6', paddingVertical: 12, paddingHorizontal: 24, borderRadius: 12 },
addBtnText: { color: '#fff', fontSize: 16, fontWeight: '600' },
fab: {
position: 'absolute',
right: 20,
bottom: 20,
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: '#8B5CF6',
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 4,
},
fabText: { color: '#fff', fontSize: 32, fontWeight: '300', marginTop: -4 },
});