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

214 lines
6.7 KiB
TypeScript

import { useState } from 'react';
import {
ActivityIndicator,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { RouteProp } from '@react-navigation/native';
import { api } from '../services/api';
import { fetchNativeEvents } from '../services/calendar';
import { addEvent, loadEvents } from '../store/eventStore';
import type { Event as CalendarEvent } from '@timetoleave/core';
import type { RootStack } from '../types/navigation';
import { useColors } from '../hooks/useColors';
type ScreenProps = {
navigation: NativeStackNavigationProp<RootStack, 'CalendarImport'>;
route: RouteProp<RootStack, 'CalendarImport'>;
};
/**
* Screen for importing events from either an ICS calendar URL or the device's
* native calendar. Deduplicates against already-imported events by ID.
*/
export function CalendarImportScreen({ navigation }: ScreenProps) {
const colors = useColors();
const [url, setUrl] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [count, setCount] = useState<number | null>(null);
const handleImport = async () => {
if (!url.trim()) {
setError('Bitte ICS-URL eingeben');
return;
}
setLoading(true);
setError(null);
setCount(null);
try {
const [events, existing] = await Promise.all([
api.fetchCalendar(url.trim()),
loadEvents(),
]);
const existingIds = new Set(existing.map((e) => e.id));
let added = 0;
for (const evt of events) {
if (!existingIds.has(evt.id)) {
const localEvent: CalendarEvent = {
id: evt.id,
title: evt.title,
destination: evt.destination,
eventTime: new Date(evt.eventTime),
source: `calendar:${url.trim().slice(0, 40)}`,
};
await addEvent(localEvent);
added++;
}
}
setCount(added);
} catch (err) {
setError(err instanceof Error ? err.message : 'Import fehlgeschlagen');
} finally {
setLoading(false);
}
};
const handleSyncNative = async () => {
setLoading(true);
setError(null);
setCount(null);
try {
// Fetch events from the next 30 days
const now = new Date();
const thirtyDaysLater = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
const nativeEvents = await fetchNativeEvents(now, thirtyDaysLater);
// Load existing events to avoid duplicates
const existing = await loadEvents();
const existingIds = new Set(existing.map((e) => e.id));
let added = 0;
for (const evt of nativeEvents) {
if (!existingIds.has(evt.id)) {
await addEvent(evt);
added++;
}
}
setCount(added);
} catch (err) {
setError(err instanceof Error ? err.message : 'Sync fehlgeschlagen');
} finally {
setLoading(false);
}
};
return (
<ScrollView style={[styles.container, { backgroundColor: colors.background }]}>
<View style={styles.content}>
<Text style={[styles.heading, { color: colors.text }]}>Kalender-Import</Text>
<Text style={[styles.description, { color: colors.subtext }]}>
Importiere Termine über eine ICS-URL oder sync mit dem Geräte-Kalender.
</Text>
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>ICS-URL Import</Text>
<TextInput
style={[styles.input, { backgroundColor: colors.card, color: colors.text, borderColor: colors.border }]}
placeholder="https://calendar.google.com/calendar/ical/..."
placeholderTextColor={colors.subtext}
value={url}
onChangeText={setUrl}
autoCapitalize="none"
keyboardType="url"
/>
<TouchableOpacity
style={[styles.importBtn, loading && styles.importBtnDisabled]}
onPress={handleImport}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.importBtnText}>ICS Importieren</Text>
)}
</TouchableOpacity>
</View>
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>Geräte-Kalender Sync</Text>
<Text style={[styles.sectionDesc, { color: colors.subtext }]}>
Hole Termine der nächsten 30 Tage aus den Kalendern auf deinem Gerät.
</Text>
<TouchableOpacity
style={[styles.importBtn, { backgroundColor: colors.purple }, loading && styles.importBtnDisabled]}
onPress={handleSyncNative}
disabled={loading}
>
<Text style={styles.importBtnText}>📅 Kalender Sync</Text>
</TouchableOpacity>
</View>
{error && (
<View style={[styles.errorBanner, { backgroundColor: colors.error }]}>
<Text style={styles.errorText}>{error}</Text>
</View>
)}
{count !== null && (
<View style={[styles.successBanner, { backgroundColor: colors.success }]}>
<Text style={styles.successText}>
{count} Termin(e) erfolgreich importiert!
</Text>
</View>
)}
<TouchableOpacity
style={styles.backBtn}
onPress={() => navigation.goBack()}
>
<Text style={[styles.backBtnText, { color: colors.accent }]}> Zurück</Text>
</TouchableOpacity>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
content: { padding: 20 },
heading: { fontSize: 22, fontWeight: '700', marginBottom: 4 },
description: { fontSize: 14, marginBottom: 20, lineHeight: 20 },
section: { marginBottom: 24 },
sectionTitle: { fontSize: 16, fontWeight: '600', marginBottom: 8 },
sectionDesc: { fontSize: 13, marginBottom: 12, lineHeight: 18 },
input: {
borderRadius: 10,
paddingHorizontal: 14,
paddingVertical: 12,
fontSize: 16,
borderWidth: 1,
marginBottom: 12,
},
errorBanner: { borderRadius: 8, padding: 12, marginBottom: 12 },
errorText: { color: '#fff', fontSize: 14 },
successBanner: { borderRadius: 8, padding: 12, marginBottom: 12 },
successText: { color: '#fff', fontSize: 14 },
importBtn: {
backgroundColor: '#8B5CF6',
paddingVertical: 14,
borderRadius: 12,
alignItems: 'center',
},
importBtnDisabled: { opacity: 0.6 },
importBtnText: { color: '#fff', fontSize: 16, fontWeight: '600' },
backBtn: {
paddingVertical: 10,
alignItems: 'center',
},
backBtnText: { fontSize: 15 },
});