First commit

This commit is contained in:
2026-06-01 23:16:10 +02:00
commit 1ea182f68d
56 changed files with 42848 additions and 0 deletions
+311
View File
@@ -0,0 +1,311 @@
import React, { useState } from 'react';
import { router } from 'expo-router';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Alert,
ActivityIndicator,
KeyboardAvoidingView,
Platform,
ScrollView,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { MaterialIcons } from '@expo/vector-icons';
import * as Device from 'expo-device';
import { useHouseholdStore } from '../src/hooks/useHousehold';
import { createHousehold, joinHousehold, getHousehold, registerMember } from '../src/services/household';
import { getFcmToken } from '../src/services/notifications';
import { COLORS } from '../src/constants';
export default function OnboardingScreen() {
const setHousehold = useHouseholdStore((s) => s.setHousehold);
const [householdName, setHouseholdName] = useState('');
const [deviceName, setDeviceName] = useState(Device.deviceName ?? '');
const [inviteLink, setInviteLink] = useState('');
const [tab, setTab] = useState<'create' | 'join'>('create');
const [loading, setLoading] = useState(false);
const handleCreate = async () => {
if (!householdName.trim()) {
Alert.alert('Pflichtfeld', 'Bitte gib einen Haushaltsnamen ein.');
return;
}
setLoading(true);
try {
const household = await createHousehold(householdName.trim());
setHousehold(household);
router.replace('/');
const fcmToken = await getFcmToken().catch(() => null);
await registerMember(household.id, fcmToken, deviceName).catch((e) =>
console.warn('registerMember failed (non-critical):', e)
);
} catch (e: any) {
Alert.alert('Fehler', 'Haushalt konnte nicht erstellt werden. Bitte Internetverbindung prüfen.');
} finally {
setLoading(false);
}
};
const handleJoin = async () => {
const raw = inviteLink.trim();
const qIndex = raw.indexOf('?');
if (!raw || qIndex === -1) {
Alert.alert('Ungültiger Link', 'Füge den vollständigen Einladungslink ein.');
return;
}
const params = new URLSearchParams(raw.slice(qIndex + 1));
const householdId = params.get('householdId') ?? '';
const token = params.get('token') ?? '';
if (!householdId || !token) {
Alert.alert('Ungültiger Link', 'Der Link enthält keine gültigen Zugangsdaten.');
return;
}
setLoading(true);
try {
const success = await joinHousehold(householdId, token);
if (!success) {
Alert.alert('Abgelaufener Link', 'Dieser Einladungslink ist ungültig oder wurde erneuert. Bitte einen neuen Link anfordern.');
return;
}
const household = await getHousehold(householdId);
if (!household) {
Alert.alert('Fehler', 'Haushalt nicht gefunden. Bitte erneut versuchen.');
return;
}
const fcmToken = await getFcmToken().catch(() => null);
await registerMember(householdId, fcmToken, deviceName);
setHousehold(household);
router.replace('/');
} catch {
Alert.alert('Verbindungsfehler', 'Bitte Internetverbindung prüfen und erneut versuchen.');
} finally {
setLoading(false);
}
};
return (
<SafeAreaView style={styles.safeArea}>
<KeyboardAvoidingView
style={styles.keyboardView}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
contentContainerStyle={styles.scroll}
keyboardShouldPersistTaps="handled"
bounces={false}
showsVerticalScrollIndicator={false}
>
<View style={styles.hero}>
<View style={styles.heroIcon}>
<MaterialIcons name="home" size={42} color={COLORS.white} />
</View>
<Text style={styles.title}>HouseOrg</Text>
<Text style={styles.subtitle}>Gemeinsam organisiert</Text>
</View>
<View style={styles.card}>
<View style={styles.tabs}>
<TouchableOpacity
style={[styles.tab, tab === 'create' && styles.tabActive]}
onPress={() => setTab('create')}
>
<Text style={[styles.tabText, tab === 'create' && styles.tabTextActive]}>
Neu erstellen
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tab, tab === 'join' && styles.tabActive]}
onPress={() => setTab('join')}
>
<Text style={[styles.tabText, tab === 'join' && styles.tabTextActive]}>
Beitreten
</Text>
</TouchableOpacity>
</View>
<View style={styles.form}>
{tab === 'create' ? (
<>
<Text style={styles.label}>Haushaltsname</Text>
<TextInput
style={styles.input}
placeholder="z. B. Familie Müller"
placeholderTextColor={COLORS.textSecondary}
value={householdName}
onChangeText={setHouseholdName}
returnKeyType="next"
autoFocus
/>
<Text style={styles.label}>Mein Gerätename</Text>
<TextInput
style={styles.input}
placeholder="z. B. Aiméns iPhone"
placeholderTextColor={COLORS.textSecondary}
value={deviceName}
onChangeText={setDeviceName}
returnKeyType="done"
onSubmitEditing={handleCreate}
/>
<TouchableOpacity
style={[styles.btn, loading && styles.btnDisabled]}
onPress={handleCreate}
disabled={loading}
>
{loading ? (
<ActivityIndicator color={COLORS.white} />
) : (
<>
<MaterialIcons name="home" size={18} color={COLORS.white} />
<Text style={styles.btnText}>Haushalt erstellen</Text>
</>
)}
</TouchableOpacity>
</>
) : (
<>
<Text style={styles.label}>Einladungslink</Text>
<TextInput
style={[styles.input, styles.inputMultiline]}
placeholder="houseorg://join?householdId=…"
placeholderTextColor={COLORS.textSecondary}
value={inviteLink}
onChangeText={setInviteLink}
multiline
autoCapitalize="none"
autoCorrect={false}
/>
<Text style={styles.label}>Mein Gerätename</Text>
<TextInput
style={styles.input}
placeholder="z. B. Aiméns iPhone"
placeholderTextColor={COLORS.textSecondary}
value={deviceName}
onChangeText={setDeviceName}
returnKeyType="done"
onSubmitEditing={handleJoin}
/>
<TouchableOpacity
style={[styles.btn, loading && styles.btnDisabled]}
onPress={handleJoin}
disabled={loading}
>
{loading ? (
<ActivityIndicator color={COLORS.white} />
) : (
<>
<MaterialIcons name="group-add" size={18} color={COLORS.white} />
<Text style={styles.btnText}>Beitreten</Text>
</>
)}
</TouchableOpacity>
</>
)}
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: { flex: 1, backgroundColor: COLORS.primary },
keyboardView: { flex: 1 },
scroll: { flexGrow: 1 },
hero: {
alignItems: 'center',
paddingTop: 56,
paddingBottom: 52,
paddingHorizontal: 24,
backgroundColor: COLORS.primary,
},
heroIcon: {
width: 84,
height: 84,
borderRadius: 26,
backgroundColor: 'rgba(255,255,255,0.18)',
justifyContent: 'center',
alignItems: 'center',
marginBottom: 20,
},
title: {
fontSize: 36,
fontWeight: '700',
color: COLORS.white,
letterSpacing: -0.5,
},
subtitle: {
fontSize: 17,
color: 'rgba(255,255,255,0.72)',
marginTop: 6,
},
card: {
flex: 1,
backgroundColor: COLORS.surface,
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
padding: 24,
paddingTop: 28,
},
tabs: {
flexDirection: 'row',
backgroundColor: COLORS.white,
borderRadius: 14,
padding: 4,
marginBottom: 26,
shadowColor: '#000',
shadowOpacity: 0.05,
shadowRadius: 4,
elevation: 1,
},
tab: {
flex: 1,
paddingVertical: 10,
borderRadius: 10,
alignItems: 'center',
},
tabActive: { backgroundColor: COLORS.primary },
tabText: { fontSize: 14, color: COLORS.textSecondary, fontWeight: '500' },
tabTextActive: { color: COLORS.white, fontWeight: '600' },
form: { gap: 12 },
label: {
fontSize: 12,
fontWeight: '600',
color: COLORS.text,
textTransform: 'uppercase',
letterSpacing: 0.5,
},
input: {
borderWidth: 1.5,
borderColor: COLORS.border,
borderRadius: 12,
padding: 14,
fontSize: 16,
backgroundColor: COLORS.white,
color: COLORS.text,
},
inputMultiline: { height: 100, textAlignVertical: 'top' },
btn: {
backgroundColor: COLORS.primary,
borderRadius: 14,
paddingVertical: 16,
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'center',
gap: 8,
marginTop: 4,
shadowColor: COLORS.primary,
shadowOpacity: 0.28,
shadowRadius: 10,
shadowOffset: { width: 0, height: 4 },
elevation: 4,
},
btnDisabled: { opacity: 0.6, shadowOpacity: 0 },
btnText: { color: COLORS.white, fontSize: 16, fontWeight: '600' },
});