Add mobile services, web proxy, and Gemma4 agent loop
- Mobile: add push notification and calendar sync services with full test suite (calendar, eventStore, notifications, screens) - Mobile: add EAS build config and Jest setup - Web: add API proxy, update useDestinationStation/useJourneys hooks, add middleware tests - Web: update next.config and rebuild - Agent: add Gemma4-based agent loop (agent_base, ttl_agent, ts_agent) - Docs: add privacy policy, post-MVP plan, and aider rules
This commit is contained in:
@@ -11,7 +11,8 @@ import {
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import type { RouteProp } from '@react-navigation/native';
|
||||
import { api } from '../services/api';
|
||||
import { addEvent } from '../store/eventStore';
|
||||
import { fetchNativeEvents } from '../services/calendar';
|
||||
import { addEvent, loadEvents } from '../store/eventStore';
|
||||
import type { Event as CalendarEvent } from '@timetoleave/core';
|
||||
|
||||
type RootStack = {
|
||||
@@ -64,21 +65,85 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.heading}>Kalender-Import</Text>
|
||||
<Text style={styles.description}>
|
||||
Gib eine ICS-Kalender-URL ein, um Termine automatisch zu importieren.
|
||||
Importiere Termine über eine ICS-URL oder sync mit dem Geräte-Kalender.
|
||||
</Text>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="https://calendar.google.com/calendar/ical/..."
|
||||
value={url}
|
||||
onChangeText={setUrl}
|
||||
autoCapitalize="none"
|
||||
keyboardType="url"
|
||||
/>
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>ICS-URL Import</Text>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="https://calendar.google.com/calendar/ical/..."
|
||||
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}>Geräte-Kalender Sync</Text>
|
||||
<Text style={styles.sectionDesc}>
|
||||
Hole Termine der nächsten 30 Tage aus den kalendern auf deinem Gerät.
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.importBtn, styles.nativeBtn, loading && styles.importBtnDisabled]}
|
||||
onPress={handleSyncNative}
|
||||
disabled={loading}
|
||||
>
|
||||
<Text style={styles.importBtnText}>📅 Kalender Sync</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{error && (
|
||||
<View style={styles.errorBanner}>
|
||||
@@ -94,18 +159,6 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
|
||||
</View>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.importBtn, loading && styles.importBtnDisabled]}
|
||||
onPress={handleImport}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.importBtnText}>Importieren</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.backBtn}
|
||||
onPress={() => navigation.goBack()}
|
||||
@@ -120,7 +173,11 @@ export function CalendarImportScreen({ navigation }: ScreenProps) {
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: '#f2f2f7' },
|
||||
content: { padding: 20 },
|
||||
description: { fontSize: 14, color: '#8e8e93', marginBottom: 16, lineHeight: 20 },
|
||||
heading: { fontSize: 22, fontWeight: '700', color: '#1c1c1e', marginBottom: 4 },
|
||||
description: { fontSize: 14, color: '#8e8e93', marginBottom: 20, lineHeight: 20 },
|
||||
section: { marginBottom: 24 },
|
||||
sectionTitle: { fontSize: 16, fontWeight: '600', color: '#1c1c1e', marginBottom: 8 },
|
||||
sectionDesc: { fontSize: 13, color: '#8e8e93', marginBottom: 12, lineHeight: 18 },
|
||||
input: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 10,
|
||||
@@ -129,7 +186,7 @@ const styles = StyleSheet.create({
|
||||
fontSize: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e5e5ea',
|
||||
marginBottom: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
errorBanner: { backgroundColor: '#FF3B30', borderRadius: 8, padding: 12, marginBottom: 12 },
|
||||
errorText: { color: '#fff', fontSize: 14 },
|
||||
@@ -140,7 +197,9 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 14,
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
nativeBtn: {
|
||||
backgroundColor: '#5856D6',
|
||||
},
|
||||
importBtnDisabled: { opacity: 0.6 },
|
||||
importBtnText: { color: '#fff', fontSize: 16, fontWeight: '600' },
|
||||
|
||||
Reference in New Issue
Block a user