WellNuo/app/(auth)/login.tsx
Sergei ec63a2c1e2 Add admin panel, optimized API, OTP auth, migrations
Admin Panel (Next.js):
- Dashboard with stats
- Users list with relationships (watches/watched_by)
- User detail pages
- Deployments list and detail pages
- Devices, Orders, Subscriptions pages
- OTP-based admin authentication

Backend Optimizations:
- Fixed N+1 query problem in admin APIs
- Added pagination support
- Added .range() and count support to Supabase wrapper
- Optimized batch queries with lookup maps

Database:
- Added migrations for schema evolution
- New tables: push_tokens, notification_settings
- Updated access model

iOS Build Scripts:
- build-ios.sh, clear-apple-cache.sh
- EAS configuration updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-20 11:05:39 -08:00

206 lines
5.4 KiB
TypeScript

import React, { useState, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
KeyboardAvoidingView,
Platform,
ScrollView,
TouchableOpacity,
Image,
} from 'react-native';
import { router } from 'expo-router';
import { useAuth } from '@/contexts/AuthContext';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { ErrorMessage } from '@/components/ui/ErrorMessage';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
export default function LoginScreen() {
const { login, isLoading, error, clearError } = useAuth();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [validationError, setValidationError] = useState<string | null>(null);
const handleLogin = useCallback(async () => {
// Clear previous errors
clearError();
setValidationError(null);
// Validate
if (!username.trim()) {
setValidationError('Username is required');
return;
}
if (!password.trim()) {
setValidationError('Password is required');
return;
}
const success = await login({ username: username.trim(), password });
if (success) {
// Clear password from memory after successful login
setPassword('');
router.replace('/(tabs)');
}
}, [username, password, login, clearError]);
const displayError = validationError || error?.message;
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Logo / Header */}
<View style={styles.header}>
<Image
source={require('@/assets/images/icon.png')}
style={styles.logo}
resizeMode="contain"
/>
<Text style={styles.title}>Welcome Back</Text>
<Text style={styles.subtitle}>Sign in to continue monitoring your loved ones</Text>
</View>
{/* Form */}
<View style={styles.form}>
{displayError && (
<ErrorMessage
message={displayError}
onDismiss={() => {
clearError();
setValidationError(null);
}}
/>
)}
<Input
label="Username"
placeholder="Enter your username"
leftIcon="person-outline"
value={username}
onChangeText={(text) => {
setUsername(text);
setValidationError(null);
}}
autoCapitalize="none"
autoCorrect={false}
editable={!isLoading}
/>
<Input
label="Password"
placeholder="Enter your password"
leftIcon="lock-closed-outline"
secureTextEntry
value={password}
onChangeText={(text) => {
setPassword(text);
setValidationError(null);
}}
editable={!isLoading}
onSubmitEditing={handleLogin}
returnKeyType="done"
/>
<TouchableOpacity style={styles.forgotPassword} onPress={() => router.push('/(auth)/forgot-password')}>
<Text style={styles.forgotPasswordText}>Forgot Password?</Text>
</TouchableOpacity>
<Button
title="Sign In"
onPress={handleLogin}
loading={isLoading}
fullWidth
size="lg"
/>
</View>
{/* Footer */}
<View style={styles.footer}>
<Text style={styles.footerText}>Don't have an account? </Text>
<TouchableOpacity onPress={() => router.push('/(auth)/register')}>
<Text style={styles.footerLink}>Create Account</Text>
</TouchableOpacity>
</View>
{/* Version Info */}
<Text style={styles.version}>WellNuo v1.0.0</Text>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
},
scrollContent: {
flexGrow: 1,
paddingHorizontal: Spacing.lg,
paddingTop: Spacing.xxl + Spacing.xl,
paddingBottom: Spacing.xl,
},
header: {
alignItems: 'center',
marginBottom: Spacing.xl,
},
logo: {
width: 180,
height: 100,
marginBottom: Spacing.lg,
},
title: {
fontSize: FontSizes['2xl'],
fontWeight: '700',
color: AppColors.textPrimary,
marginBottom: Spacing.xs,
},
subtitle: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textAlign: 'center',
},
form: {
marginBottom: Spacing.xl,
},
forgotPassword: {
alignSelf: 'flex-end',
marginBottom: Spacing.lg,
marginTop: -Spacing.sm,
},
forgotPasswordText: {
fontSize: FontSizes.sm,
color: AppColors.primary,
fontWeight: '500',
},
footer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.xl,
},
footerText: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
},
footerLink: {
fontSize: FontSizes.base,
color: AppColors.primary,
fontWeight: '600',
},
version: {
textAlign: 'center',
fontSize: FontSizes.xs,
color: AppColors.textMuted,
},
});