214 lines
6.7 KiB
TypeScript
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 },
|
|
});
|