209 lines
7.3 KiB
TypeScript
209 lines
7.3 KiB
TypeScript
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 },
|
||
});
|