React Native Expo app — full mobile client
- Expo SDK 54, expo-router, NativeWind - 7 screens: login, tasks, calendar, goals, chat, settings, tabs - API client (api.hasdo.info), SecureStore auth, AuthContext - Tab navigation: Ukoly, Kalendar, Cile, Chat, Nastaveni - Pull-to-refresh, FAB, group colors, priority dots - Calendar grid with task dots - AI chat with keyboard avoiding - Web export verified (780 modules, 1.5MB bundle) - Bundle ID: info.hasdo.taskteam Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
256
mobile/app/login.tsx
Normal file
256
mobile/app/login.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useAuthContext } from '../lib/AuthContext';
|
||||
|
||||
export default function LoginScreen() {
|
||||
const { login, register } = useAuthContext();
|
||||
const [mode, setMode] = useState<'login' | 'register'>('login');
|
||||
const [email, setEmail] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!email.trim() || !password.trim()) {
|
||||
Alert.alert('Chyba', 'Vyplnte vsechna pole');
|
||||
return;
|
||||
}
|
||||
if (mode === 'register' && !name.trim()) {
|
||||
Alert.alert('Chyba', 'Vyplnte jmeno');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
if (mode === 'login') {
|
||||
await login(email.trim(), password);
|
||||
} else {
|
||||
await register(email.trim(), name.trim(), password);
|
||||
}
|
||||
} catch (err: any) {
|
||||
Alert.alert('Chyba', err.message || 'Nepodarilo se prihlasit');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{/* Logo */}
|
||||
<View style={styles.logoSection}>
|
||||
<View style={styles.logoCircle}>
|
||||
<Ionicons name="checkbox" size={40} color="#fff" />
|
||||
</View>
|
||||
<Text style={styles.appName}>Task Team</Text>
|
||||
<Text style={styles.appDesc}>
|
||||
Spravujte ukoly, cile a tymovou spolupraci
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Form */}
|
||||
<View style={styles.form}>
|
||||
{/* Mode toggle */}
|
||||
<View style={styles.modeToggle}>
|
||||
<TouchableOpacity
|
||||
style={[styles.modeBtn, mode === 'login' && styles.modeBtnActive]}
|
||||
onPress={() => setMode('login')}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.modeBtnText,
|
||||
mode === 'login' && styles.modeBtnTextActive,
|
||||
]}
|
||||
>
|
||||
Prihlaseni
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.modeBtn, mode === 'register' && styles.modeBtnActive]}
|
||||
onPress={() => setMode('register')}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.modeBtnText,
|
||||
mode === 'register' && styles.modeBtnTextActive,
|
||||
]}
|
||||
>
|
||||
Registrace
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{mode === 'register' && (
|
||||
<View style={styles.inputGroup}>
|
||||
<Ionicons
|
||||
name="person-outline"
|
||||
size={20}
|
||||
color="#94A3B8"
|
||||
style={styles.inputIcon}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Jmeno"
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
autoCapitalize="words"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.inputGroup}>
|
||||
<Ionicons
|
||||
name="mail-outline"
|
||||
size={20}
|
||||
color="#94A3B8"
|
||||
style={styles.inputIcon}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputGroup}>
|
||||
<Ionicons
|
||||
name="lock-closed-outline"
|
||||
size={20}
|
||||
color="#94A3B8"
|
||||
style={styles.inputIcon}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.input, { flex: 1 }]}
|
||||
placeholder="Heslo"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry={!showPassword}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowPassword(!showPassword)}
|
||||
style={styles.eyeBtn}
|
||||
>
|
||||
<Ionicons
|
||||
name={showPassword ? 'eye-off-outline' : 'eye-outline'}
|
||||
size={20}
|
||||
color="#94A3B8"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.submitBtn, loading && styles.submitBtnDisabled]}
|
||||
onPress={handleSubmit}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.submitBtnText}>
|
||||
{mode === 'login' ? 'Prihlasit se' : 'Zaregistrovat se'}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: '#F8FAFC' },
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
padding: 24,
|
||||
},
|
||||
logoSection: { alignItems: 'center', marginBottom: 40 },
|
||||
logoCircle: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#3B82F6',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
appName: { fontSize: 28, fontWeight: '800', color: '#1E293B' },
|
||||
appDesc: {
|
||||
fontSize: 15,
|
||||
color: '#64748B',
|
||||
textAlign: 'center',
|
||||
marginTop: 8,
|
||||
},
|
||||
form: {},
|
||||
modeToggle: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#E2E8F0',
|
||||
borderRadius: 12,
|
||||
padding: 4,
|
||||
marginBottom: 24,
|
||||
},
|
||||
modeBtn: {
|
||||
flex: 1,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
},
|
||||
modeBtnActive: { backgroundColor: '#fff' },
|
||||
modeBtnText: { fontSize: 15, color: '#64748B', fontWeight: '500' },
|
||||
modeBtnTextActive: { color: '#3B82F6', fontWeight: '700' },
|
||||
inputGroup: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E2E8F0',
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 14,
|
||||
},
|
||||
inputIcon: { marginRight: 10 },
|
||||
input: {
|
||||
flex: 1,
|
||||
paddingVertical: 14,
|
||||
fontSize: 16,
|
||||
color: '#1E293B',
|
||||
},
|
||||
eyeBtn: { padding: 4 },
|
||||
submitBtn: {
|
||||
backgroundColor: '#3B82F6',
|
||||
borderRadius: 12,
|
||||
paddingVertical: 16,
|
||||
alignItems: 'center',
|
||||
marginTop: 8,
|
||||
shadowColor: '#3B82F6',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
submitBtnDisabled: { backgroundColor: '#94A3B8' },
|
||||
submitBtnText: { color: '#fff', fontSize: 17, fontWeight: '700' },
|
||||
});
|
||||
Reference in New Issue
Block a user