First commit
This commit is contained in:
@@ -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' },
|
||||
});
|
||||
Reference in New Issue
Block a user