diff --git a/apps/frontend-mobile/app/(tabs)/alerts.tsx b/apps/frontend-mobile/app/(tabs)/alerts.tsx index 7dafed3..949ead5 100644 --- a/apps/frontend-mobile/app/(tabs)/alerts.tsx +++ b/apps/frontend-mobile/app/(tabs)/alerts.tsx @@ -1,7 +1,9 @@ import React, { useEffect, useState } from 'react'; import { View, Text, StyleSheet, FlatList, TouchableOpacity, Alert, useWindowDimensions } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; +import { useRouter } from 'expo-router'; import { useThemeContext } from '../../components/ThemeProvider'; +import { useAuth } from '../../hooks/useAuth'; import { spacing, borderRadius, shadows } from '../../constants/theme'; import { LoadingSpinner } from '../../components/LoadingSpinner'; import api from '../../services/api'; @@ -20,17 +22,23 @@ interface NotificationItem { } export default function AlertsScreen() { + const router = useRouter(); const { width } = useWindowDimensions(); const isTablet = width >= TABLET_MIN_WIDTH; const { colors } = useThemeContext(); + const { isAuthenticated, isLoading: authLoading } = useAuth(); const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [deletingId, setDeletingId] = useState(null); useEffect(() => { - loadNotifications(); - }, []); + if (!authLoading && isAuthenticated) { + loadNotifications(); + } else if (!authLoading && !isAuthenticated) { + setIsLoading(false); + } + }, [authLoading, isAuthenticated]); async function loadNotifications() { setIsLoading(true); @@ -123,10 +131,28 @@ export default function AlertsScreen() { ); } - if (isLoading) { + if (isLoading || authLoading) { return ; } + if (!isAuthenticated) { + return ( + + + Inicia sesión para continuar + + Necesitas estar autenticado para ver tus notificaciones + + router.push('/auth/login')} + > + Iniciar Sesión + + + ); + } + return ( @@ -172,6 +198,32 @@ const styles = StyleSheet.create({ container: { flex: 1, }, + centered: { + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: spacing.xl, + gap: spacing.md, + }, + loginTitle: { + fontSize: 20, + fontWeight: '600', + textAlign: 'center', + }, + loginSubtitle: { + fontSize: 14, + textAlign: 'center', + lineHeight: 20, + }, + loginButton: { + paddingHorizontal: spacing.lg, + paddingVertical: spacing.md, + borderRadius: borderRadius.lg, + marginTop: spacing.sm, + }, + loginButtonText: { + fontSize: 16, + fontWeight: '600', + }, header: { paddingHorizontal: spacing.lg, paddingTop: spacing.lg, diff --git a/apps/frontend-mobile/app/(tabs)/profile.tsx b/apps/frontend-mobile/app/(tabs)/profile.tsx index 37b4848..0af19a0 100644 --- a/apps/frontend-mobile/app/(tabs)/profile.tsx +++ b/apps/frontend-mobile/app/(tabs)/profile.tsx @@ -6,7 +6,7 @@ import * as ImagePicker from 'expo-image-picker'; import { useAuth } from '../../hooks/useAuth'; import { useThemeContext } from '../../components/ThemeProvider'; import { useThemeStore, ThemeMode } from '../../store/themeStore'; -import { spacing, borderRadius } from '../../constants/theme'; +import { spacing, borderRadius, shadows } from '../../constants/theme'; import api from '../../services/api'; const TABLET_MIN_WIDTH = 768; @@ -84,11 +84,9 @@ export default function ProfileScreen() { const [avatarLocalSource, setAvatarLocalSource] = useState(initialResolved); const [searchHistory, setSearchHistory] = useState([]); - // Avatar modal state const [showAvatarModal, setShowAvatarModal] = useState(false); const [avatarTab, setAvatarTab] = useState<'presets' | 'colors' | 'upload'>('presets'); - // Config modal state const [showConfig, setShowConfig] = useState(false); const [configFirstName, setConfigFirstName] = useState(''); const [configLastName, setConfigLastName] = useState(''); @@ -98,7 +96,6 @@ export default function ProfileScreen() { const [configSaving, setConfigSaving] = useState(false); const [configFeedback, setConfigFeedback] = useState<{ type: 'ok' | 'err'; text: string } | null>(null); - // Addresses modal state const [showAddresses, setShowAddresses] = useState(false); const [addresses, setAddresses] = useState([]); const [addressesLoading, setAddressesLoading] = useState(false); @@ -111,31 +108,19 @@ export default function ProfileScreen() { const [formError, setFormError] = useState(''); useEffect(() => { - if (isAuthenticated) { - loadSearchHistory(); - } + if (isAuthenticated) loadSearchHistory(); }, [isAuthenticated]); useEffect(() => { setFirstName(user?.first_name || ''); setLastName(user?.last_name || ''); const resolved = resolveAvatarUrl(user?.avatar_url); - if (resolved) { - setAvatarLocalSource(resolved); - setAvatarUrl(''); - } else { - setAvatarLocalSource(null); - setAvatarUrl(user?.avatar_url || ''); - } + if (resolved) { setAvatarLocalSource(resolved); setAvatarUrl(''); } + else { setAvatarLocalSource(null); setAvatarUrl(user?.avatar_url || ''); } }, [user]); async function loadSearchHistory() { - try { - const res = await api.get('/search-history'); - setSearchHistory(res.data); - } catch (err) { - console.error('Error loading search history:', err); - } + try { const res = await api.get('/search-history'); setSearchHistory(res.data); } catch {} } function openConfig() { @@ -159,226 +144,89 @@ export default function ProfileScreen() { city: configCity.trim() || null, address: configAddress.trim() || null, }); - const updated = res.data; - setFirstName(updated.first_name || ''); - setLastName(updated.last_name || ''); + setFirstName(res.data.first_name || ''); + setLastName(res.data.last_name || ''); setConfigFeedback({ type: 'ok', text: 'Perfil guardado.' }); setTimeout(() => setShowConfig(false), 1200); } catch (err: any) { setConfigFeedback({ type: 'err', text: err.message || 'Error al guardar' }); - } finally { - setConfigSaving(false); - } + } finally { setConfigSaving(false); } } async function handlePickImage() { - const result = await ImagePicker.launchImageLibraryAsync({ - mediaTypes: ImagePicker.MediaTypeOptions.Images, - allowsEditing: true, - aspect: [1, 1], - quality: 0.8, - base64: true, - }); - - if (!result.canceled && result.assets[0]) { - const asset = result.assets[0]; - if (asset.base64) { - const dataUri = `data:${asset.mimeType};base64,${asset.base64}`; - setAvatarLocalSource(null); - setAvatarUrl(dataUri); - setShowAvatarModal(false); - try { - await api.put('/users/me', { avatar_url: dataUri }); - } catch (err) { - console.error('Error saving avatar:', err); - } - } + const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, aspect: [1, 1], quality: 0.8, base64: true }); + if (!result.canceled && result.assets[0]?.base64) { + const dataUri = `data:${result.assets[0].mimeType};base64,${result.assets[0].base64}`; + setAvatarLocalSource(null); setAvatarUrl(dataUri); setShowAvatarModal(false); + try { await api.put('/users/me', { avatar_url: dataUri }); } catch {} } } async function handleTakePhoto() { const { status } = await ImagePicker.requestCameraPermissionsAsync(); - if (status !== 'granted') { - Alert.alert('Permiso requerido', 'Necesitamos permiso para acceder a la cámara.'); - return; - } - - const result = await ImagePicker.launchCameraAsync({ - allowsEditing: true, - aspect: [1, 1], - quality: 0.8, - base64: true, - }); - - if (!result.canceled && result.assets[0]) { - const asset = result.assets[0]; - if (asset.base64) { - const dataUri = `data:${asset.mimeType};base64,${asset.base64}`; - setAvatarLocalSource(null); - setAvatarUrl(dataUri); - setShowAvatarModal(false); - try { - await api.put('/users/me', { avatar_url: dataUri }); - } catch (err) { - console.error('Error saving avatar:', err); - } - } + if (status !== 'granted') { Alert.alert('Permiso requerido', 'Necesitamos permiso para acceder a la cámara.'); return; } + const result = await ImagePicker.launchCameraAsync({ allowsEditing: true, aspect: [1, 1], quality: 0.8, base64: true }); + if (!result.canceled && result.assets[0]?.base64) { + const dataUri = `data:${result.assets[0].mimeType};base64,${result.assets[0].base64}`; + setAvatarLocalSource(null); setAvatarUrl(dataUri); setShowAvatarModal(false); + try { await api.put('/users/me', { avatar_url: dataUri }); } catch {} } } async function handleSelectPresetAvatar(index: number) { - const avatarSource = AVATARS[index]; - setAvatarUrl(''); - setAvatarLocalSource(avatarSource); - setShowAvatarModal(false); - try { - await api.put('/users/me', { avatar_url: `preset_avatar_${index + 1}` }); - } catch (err) { - console.error('Error saving avatar:', err); - } + setAvatarUrl(''); setAvatarLocalSource(AVATARS[index]); setShowAvatarModal(false); + try { await api.put('/users/me', { avatar_url: `preset_avatar_${index + 1}` }); } catch {} } async function handleSelectColor(index: number) { - const colorSource = COLOR_CIRCLES[index]; - setAvatarUrl(''); - setAvatarLocalSource(colorSource); - setShowAvatarModal(false); - try { - await api.put('/users/me', { avatar_url: `color_circle_${index + 1}` }); - } catch (err) { - console.error('Error saving avatar:', err); - } + setAvatarUrl(''); setAvatarLocalSource(COLOR_CIRCLES[index]); setShowAvatarModal(false); + try { await api.put('/users/me', { avatar_url: `color_circle_${index + 1}` }); } catch {} } async function handleDeleteSearch(id: number) { - try { - await api.delete(`/search-history/${id}`); - setSearchHistory(prev => prev.filter(item => item.id !== id)); - } catch (err) { - console.error('Error deleting search:', err); - } + try { await api.delete(`/search-history/${id}`); setSearchHistory(prev => prev.filter(i => i.id !== id)); } catch {} } - // Addresses async function loadAddresses() { setAddressesLoading(true); - try { - const res = await api.get('/addresses'); - setAddresses(res.data); - } catch (err) { - console.error('Error loading addresses:', err); - } finally { - setAddressesLoading(false); - } + try { const res = await api.get('/addresses'); setAddresses(res.data); } catch {} finally { setAddressesLoading(false); } } - function openAddresses() { - setShowAddresses(true); - loadAddresses(); - } + function openAddresses() { setShowAddresses(true); loadAddresses(); } function openAddAddressForm() { - setEditingAddressId(null); - setFormAddress(''); - setFormLabel(''); - setFormDefault(addresses.length === 0); - setFormError(''); - setShowAddressForm(true); + setEditingAddressId(null); setFormAddress(''); setFormLabel(''); setFormDefault(addresses.length === 0); setFormError(''); setShowAddressForm(true); } function openEditAddressForm(addr: Address) { - setEditingAddressId(addr.id); - setFormAddress(addr.address); - setFormLabel(addr.label || ''); - setFormDefault(Boolean(addr.is_default)); - setFormError(''); - setShowAddressForm(true); + setEditingAddressId(addr.id); setFormAddress(addr.address); setFormLabel(addr.label || ''); setFormDefault(Boolean(addr.is_default)); setFormError(''); setShowAddressForm(true); } async function handleAddressSave() { const addr = formAddress.trim(); - if (!addr) { - setFormError('La dirección es obligatoria'); - return; - } - setFormSaving(true); - setFormError(''); + if (!addr) { setFormError('La dirección es obligatoria'); return; } + setFormSaving(true); setFormError(''); try { const url = editingAddressId ? `/api/addresses/${editingAddressId}` : '/api/addresses'; - const method = editingAddressId ? 'PUT' : 'POST'; - const res = await fetch(url, { - method, - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', - body: JSON.stringify({ - address: addr, - label: formLabel.trim(), - is_default: formDefault, - }), - }); - if (!res.ok) { - const err = await res.json().catch(() => ({})); - throw new Error(err.error || 'Error al guardar la dirección'); - } - setShowAddressForm(false); - setEditingAddressId(null); - loadAddresses(); - } catch (err: any) { - setFormError(err.message); - } finally { - setFormSaving(false); - } + const res = await fetch(url, { method: editingAddressId ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ address: addr, label: formLabel.trim(), is_default: formDefault }) }); + if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error || 'Error al guardar'); } + setShowAddressForm(false); setEditingAddressId(null); loadAddresses(); + } catch (err: any) { setFormError(err.message); } finally { setFormSaving(false); } } async function handleDeleteAddress(id: number) { - try { - const res = await fetch(`/api/addresses/${id}`, { - method: 'DELETE', - credentials: 'include', - }); - if (res.ok) { - setAddresses(prev => prev.filter(a => a.id !== id)); - } - } catch (err) { - console.error('Error deleting address:', err); - } + try { const res = await fetch(`/api/addresses/${id}`, { method: 'DELETE', credentials: 'include' }); if (res.ok) setAddresses(prev => prev.filter(a => a.id !== id)); } catch {} } async function handleSetDefault(id: number) { - try { - const res = await fetch(`/api/addresses/${id}/default`, { - method: 'PUT', - credentials: 'include', - }); - if (res.ok) { - loadAddresses(); - } - } catch (err) { - console.error('Error setting default address:', err); - } + try { const res = await fetch(`/api/addresses/${id}/default`, { method: 'PUT', credentials: 'include' }); if (res.ok) loadAddresses(); } catch {} } - const handleLogout = async () => { - Alert.alert( - 'Cerrar Sesión', - '¿Estás seguro que deseas cerrar sesión?', - [ - { text: 'Cancelar', style: 'cancel' }, - { - text: 'Cerrar Sesión', - style: 'destructive', - onPress: async () => { - await logout(); - router.replace('/auth/login'); - } - }, - ] - ); - }; - - const getThemeModeLabel = () => { - const opt = THEME_OPTIONS.find(o => o.mode === themeMode); - return opt?.label || 'Sistema'; + const handleLogout = () => { + Alert.alert('Cerrar Sesión', '¿Estás seguro que deseas cerrar sesión?', [ + { text: 'Cancelar', style: 'cancel' }, + { text: 'Cerrar Sesión', style: 'destructive', onPress: async () => { await logout(); router.replace('/auth/login'); } }, + ]); }; const getThemeModeIcon = (): 'phone-portrait-outline' | 'sunny-outline' | 'moon-outline' => { @@ -388,233 +236,217 @@ export default function ProfileScreen() { }; if (isLoading) { - return ( - - Cargando... - - ); + return ; } if (!isAuthenticated) { return ( - + + + Inicia Sesión - Inicia sesión para acceder a todas las funcionalidades + Inicia sesión para acceder a tu perfil, notificaciones y más - router.push('/auth/login')} - > - Iniciar Sesión - - router.push('/auth/register')} - > - Crear cuenta nueva + router.push('/auth/login')}> + Iniciar Sesión ); } + const displayName = [firstName, lastName].filter(Boolean).join(' ') || user?.username; + return ( - - {/* Avatar Section */} - - setShowAvatarModal(true)}> + + {/* Header card with avatar + name */} + + setShowAvatarModal(true)}> {avatarLocalSource ? ( - + ) : avatarUrl ? ( - + ) : ( - + + + )} - - + + - Toca para cambiar foto + {displayName} + @{user?.username} - {/* Read-only Name Section */} - - - Nombre - {firstName || '—'} + {/* Info section */} + {(firstName || lastName) && ( + + + + Datos personales + + + + Nombre + {firstName || '—'} + + + Apellidos + {lastName || '—'} + + - - Apellidos - {lastName || '—'} + )} + + {/* Theme card */} + + + + Apariencia + + + {THEME_OPTIONS.map((opt) => ( + setThemeMode(opt.mode)} + > + + {opt.label} + + ))} - {/* Menu Section */} - - {/* Theme Toggle */} - - - - Modo de pantalla + {/* Menu card */} + + + + - - {THEME_OPTIONS.map((opt) => ( - setThemeMode(opt.mode)} - > - - - {opt.label} - - - ))} - - - - - - Configuración - + Configuración + - - - Mis Direcciones - - + - {searchHistory.length > 0 && ( - - Tus búsquedas recientes: - {searchHistory.map((item) => ( - - {item.address} - handleDeleteSearch(item.id)} - style={styles.searchDelete} - > - - - - ))} + + + - )} + Mis Direcciones + + {isAdmin && ( - - - Panel Admin - - + <> + + + + + + Panel Admin + + + )} - {/* Logout Button */} - - + {/* Search history */} + {searchHistory.length > 0 && ( + + + + Búsquedas recientes + + {searchHistory.map((item, i) => ( + + {i > 0 && } + + + {item.address} + handleDeleteSearch(item.id)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}> + + + + + ))} + + )} + + {/* Logout */} + + Cerrar Sesión - {/* Avatar Selection Modal */} + + + {/* ─── Modals ─── */} + + {/* Avatar Modal */} - - + + + Cambiar Avatar - setShowAvatarModal(false)}> + setShowAvatarModal(false)} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}> - - {/* Tab Buttons */} - - setAvatarTab('presets')} - > - - Prediseñado - - setAvatarTab('colors')} - > - - Colores - - setAvatarTab('upload')} - > - - Subir - + + {(['presets', 'colors', 'upload'] as const).map((t) => ( + setAvatarTab(t)}> + + {t === 'presets' ? 'Prediseñado' : t === 'colors' ? 'Colores' : 'Subir'} + + ))} - - {/* Tab Content */} - + {avatarTab === 'presets' && ( - {AVATARS.map((avatar, index) => ( - handleSelectPresetAvatar(index)} - > - + {AVATARS.map((av, i) => ( + handleSelectPresetAvatar(i)}> + ))} )} - {avatarTab === 'colors' && ( - {COLOR_CIRCLES.map((color, index) => ( - handleSelectColor(index)} - > - + {COLOR_CIRCLES.map((c, i) => ( + handleSelectColor(i)}> + ))} )} - {avatarTab === 'upload' && ( - - - - + + + + - Tomar foto - Usa la cámara de tu dispositivo + + Tomar foto + Usa la cámara de tu dispositivo + + - - - + + + - Elegir de galería - Selecciona una imagen existente + + Elegir de galería + Selecciona una imagen existente + + )} @@ -626,96 +458,53 @@ export default function ProfileScreen() { {/* Config Modal */} - - + + + Configuración - setShowConfig(false)}> + setShowConfig(false)} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}> - - - Nombre - - - - Apellidos - - - - Correo electrónico - - - - Ciudad - - - - Dirección - - + + {[ + { label: 'Nombre', value: configFirstName, onChange: setConfigFirstName, placeholder: 'Tu nombre', icon: 'person-outline' }, + { label: 'Apellidos', value: configLastName, onChange: setConfigLastName, placeholder: 'Tus apellidos', icon: 'person-outline' }, + { label: 'Correo electrónico', value: configEmail, onChange: setConfigEmail, placeholder: 'tu@email.com', icon: 'mail-outline', keyboard: 'email-address' as const }, + { label: 'Ciudad', value: configCity, onChange: setConfigCity, placeholder: 'Tu ciudad', icon: 'business-outline' }, + { label: 'Dirección', value: configAddress, onChange: setConfigAddress, placeholder: 'Calle Mayor 1, Madrid', icon: 'location-outline' }, + ].map((field) => ( + + {field.label} + + + + + + ))} {configFeedback && ( - + + {configFeedback.text} )} - setShowConfig(false)} - disabled={configSaving} - > + setShowConfig(false)} disabled={configSaving}> Cancelar - - {configSaving ? ( - - ) : ( - Guardar - )} + + {configSaving ? : Guardar} @@ -726,49 +515,34 @@ export default function ProfileScreen() { {/* Addresses Modal */} - - + + + Mis Direcciones - { setShowAddresses(false); setShowAddressForm(false); }}> + { setShowAddresses(false); setShowAddressForm(false); }} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}> - + {showAddressForm ? ( Dirección - + + + + Etiqueta (opcional) - + + + + - setFormDefault(!formDefault)} - disabled={formSaving} - > - - Dirección predeterminada + setFormDefault(!formDefault)} disabled={formSaving}> + + Dirección predeterminada {formError ? ( @@ -776,68 +550,56 @@ export default function ProfileScreen() { ) : null} - { setShowAddressForm(false); setFormError(''); }} - disabled={formSaving} - > + { setShowAddressForm(false); setFormError(''); }} disabled={formSaving}> Cancelar - - {formSaving ? ( - - ) : ( - {editingAddressId ? 'Actualizar' : 'Añadir'} - )} + + {formSaving ? : {editingAddressId ? 'Actualizar' : 'Añadir'}} ) : ( {addressesLoading ? ( - Cargando direcciones... + ) : ( - - {user?.address ? ( - - - Dirección principal - {user.address} - - Predeterminada - + + {user?.address && ( + + + Principal + {user.address} + - ) : null} + )} {addresses.map((addr) => ( - - - {addr.label ? {addr.label} : null} - {addr.address} - handleSetDefault(addr.id)}> - Establecer como predeterminada - + + + {addr.label ? {addr.label} : null} + {addr.address} + {!addr.is_default && ( + handleSetDefault(addr.id)}> + Marcar como predeterminada + + )} - - openEditAddressForm(addr)}> - + + openEditAddressForm(addr)}> + - handleDeleteAddress(addr.id)}> - + handleDeleteAddress(addr.id)}> + ))} + + + Añadir dirección + )} - - - Añadir más - )} @@ -849,456 +611,86 @@ export default function ProfileScreen() { } const styles = StyleSheet.create({ - container: { - flex: 1, - }, - loadingText: { - textAlign: 'center', - marginTop: spacing.xxl, - }, - authPrompt: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: spacing.xl, - }, - authTitle: { - fontSize: 24, - fontWeight: 'bold', - marginTop: spacing.lg, - }, - authTitleTablet: { - fontSize: 32, - }, - authSubtitle: { - fontSize: 16, - textAlign: 'center', - marginTop: spacing.sm, - marginBottom: spacing.xl, - }, - authSubtitleTablet: { - fontSize: 18, - maxWidth: 400, - }, - avatarSection: { - alignItems: 'center', - padding: spacing.xl, - }, - avatarSectionTablet: { - paddingVertical: spacing.xxl, - }, - avatarContainer: { - width: 100, - height: 100, - borderRadius: 50, - justifyContent: 'center', - alignItems: 'center', - overflow: 'hidden', - }, - avatarContainerTablet: { - width: 140, - height: 140, - borderRadius: 70, - }, - avatarImage: { - width: 100, - height: 100, - borderRadius: 50, - }, - avatarImageTablet: { - width: 140, - height: 140, - borderRadius: 70, - }, - avatarOverlay: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: 'rgba(0, 0, 0, 0.4)', - justifyContent: 'center', - alignItems: 'center', - opacity: 0.8, - }, - editAvatarText: { - marginTop: spacing.sm, - fontSize: 14, - }, - infoSection: { - padding: spacing.lg, - marginTop: spacing.sm, - gap: spacing.md, - }, - infoSectionTablet: { - maxWidth: 600, - alignSelf: 'center', - width: '100%', - }, - infoCard: { - padding: spacing.md, - borderRadius: borderRadius.lg, - borderWidth: 1, - }, - infoLabel: { - fontSize: 12, - fontWeight: '600', - textTransform: 'uppercase', - letterSpacing: 0.05, - marginBottom: 4, - }, - infoValue: { - fontSize: 16, - fontWeight: '500', - }, - menuSection: { - marginTop: spacing.sm, - }, - menuSectionTablet: { - maxWidth: 600, - alignSelf: 'center', - width: '100%', - }, - menuItem: { - flexDirection: 'row', - alignItems: 'center', - padding: spacing.md, - borderBottomWidth: 1, - }, - menuText: { - flex: 1, - marginLeft: spacing.md, - fontSize: 16, - }, - // Theme section - themeSection: { - padding: spacing.md, - borderBottomWidth: 1, - }, - themeHeader: { - flexDirection: 'row', - alignItems: 'center', - gap: spacing.md, - marginBottom: spacing.md, - }, - themeTitle: { - fontSize: 16, - fontWeight: '500', - }, - themeOptions: { - flexDirection: 'row', - gap: spacing.sm, - }, - themeOption: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: spacing.xs, - paddingVertical: spacing.sm + 2, - paddingHorizontal: spacing.sm, - borderRadius: borderRadius.md, - borderWidth: 1, - }, - themeOptionText: { - fontSize: 13, - fontWeight: '500', - }, - themeOptionTextActive: { - fontWeight: '700', - }, - searchHistorySection: { - padding: spacing.md, - borderBottomWidth: 1, - }, - searchHistoryTitle: { - fontSize: 14, - marginBottom: spacing.sm, - }, - searchItem: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingVertical: spacing.sm, - borderBottomWidth: 1, - }, - searchAddress: { - flex: 1, - fontSize: 14, - }, - searchDelete: { - padding: spacing.xs, - }, - button: { - backgroundColor: '#7fbf8f', - borderRadius: borderRadius.lg, - paddingVertical: spacing.md, - paddingHorizontal: spacing.xl, - }, - buttonTablet: { - paddingVertical: spacing.lg, - paddingHorizontal: spacing.xl * 1.5, - }, - buttonText: { - color: '#ffffff', - fontSize: 16, - fontWeight: '600', - }, - buttonTextTablet: { - fontSize: 18, - }, - linkButton: { - marginTop: spacing.md, - }, - linkText: { - fontSize: 14, - }, - logoutButton: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - margin: spacing.xl, - padding: spacing.md, - borderRadius: borderRadius.lg, - borderWidth: 1, - }, - logoutButtonTablet: { - maxWidth: 600, - alignSelf: 'center', - width: '100%', - }, - logoutText: { - marginLeft: spacing.sm, - fontSize: 16, - fontWeight: '500', - }, - // Modal styles - modalBackdrop: { - flex: 1, - backgroundColor: 'rgba(0, 0, 0, 0.5)', - justifyContent: 'flex-end', - }, - modalContent: { - borderTopLeftRadius: 20, - borderTopRightRadius: 20, - maxHeight: '90%', - }, - modalHeader: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - padding: spacing.lg, - borderBottomWidth: 1, - }, - modalTitle: { - fontSize: 18, - fontWeight: 'bold', - }, - modalBody: { - padding: spacing.lg, - }, - modalField: { - marginBottom: spacing.md, - }, - modalLabel: { - fontSize: 13, - fontWeight: '600', - marginBottom: spacing.xs, - }, - modalInput: { - borderWidth: 1, - borderRadius: borderRadius.lg, - padding: spacing.md, - fontSize: 16, - }, - modalFeedback: { - padding: spacing.md, - borderRadius: borderRadius.lg, - marginBottom: spacing.md, - borderWidth: 1, - }, - modalFeedbackText: { - fontSize: 14, - }, - modalActions: { - flexDirection: 'row', - justifyContent: 'flex-end', - gap: spacing.md, - marginTop: spacing.md, - }, - modalCancelBtn: { - paddingVertical: spacing.md, - paddingHorizontal: spacing.lg, - borderRadius: borderRadius.lg, - borderWidth: 1, - }, - modalCancelText: { - fontSize: 16, - }, - modalSaveBtn: { - backgroundColor: '#7fbf8f', - paddingVertical: spacing.md, - paddingHorizontal: spacing.xl, - borderRadius: borderRadius.lg, - minWidth: 100, - alignItems: 'center', - }, - modalSaveBtnDisabled: { - opacity: 0.6, - }, - modalSaveText: { - color: '#ffffff', - fontSize: 16, - fontWeight: '600', - }, - // Avatar Modal styles - avatarModalContent: { - borderTopLeftRadius: 20, - borderTopRightRadius: 20, - maxHeight: '80%', - }, - tabContainer: { - flexDirection: 'row', - borderBottomWidth: 1, - }, - tabButton: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingVertical: spacing.md, - gap: spacing.xs, - }, - tabText: { - fontSize: 14, - }, - tabContent: { - padding: spacing.lg, - }, - avatarGrid: { - flexDirection: 'row', - flexWrap: 'wrap', - justifyContent: 'space-between', - gap: spacing.md, - }, - avatarOption: { - width: '30%', - aspectRatio: 1, - }, - avatarOptionImage: { - width: '100%', - height: '100%', - borderRadius: borderRadius.lg, - }, - uploadSection: { - gap: spacing.md, - }, - uploadOption: { - flexDirection: 'row', - alignItems: 'center', - padding: spacing.md, - borderRadius: borderRadius.lg, - borderWidth: 1, - }, - uploadIconContainer: { - width: 56, - height: 56, - borderRadius: 28, - justifyContent: 'center', - alignItems: 'center', - marginRight: spacing.md, - }, - uploadOptionTitle: { - fontSize: 16, - fontWeight: '600', - flex: 1, - }, - uploadOptionSubtitle: { - fontSize: 13, - flex: 1, - }, + container: { flex: 1 }, + // Auth prompt + authPrompt: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: spacing.xl, gap: spacing.md }, + authIconCircle: { width: 100, height: 100, borderRadius: 50, justifyContent: 'center', alignItems: 'center' }, + authTitle: { fontSize: 24, fontWeight: '700', marginTop: spacing.sm }, + authTitleTablet: { fontSize: 32 }, + authSubtitle: { fontSize: 15, textAlign: 'center', lineHeight: 22 }, + authSubtitleTablet: { fontSize: 17, maxWidth: 400 }, + authButton: { paddingHorizontal: spacing.xl, paddingVertical: spacing.md, borderRadius: borderRadius.lg, marginTop: spacing.sm }, + authButtonText: { fontSize: 16, fontWeight: '700' }, + // Header card + headerCard: { alignItems: 'center', marginHorizontal: spacing.lg, marginTop: spacing.lg, borderRadius: borderRadius.xl, paddingVertical: spacing.xl, gap: spacing.xs }, + avatarRing: { width: 104, height: 104, borderRadius: 52, borderWidth: 3, padding: 2, overflow: 'hidden' }, + avatarImage: { width: '100%', height: '100%', borderRadius: 50 }, + avatarFallback: { width: '100%', height: '100%', borderRadius: 50, justifyContent: 'center', alignItems: 'center' }, + avatarEditBadge: { position: 'absolute', bottom: 2, right: 2, width: 28, height: 28, borderRadius: 14, justifyContent: 'center', alignItems: 'center', borderWidth: 2, borderColor: '#fff' }, + displayName: { fontSize: 20, fontWeight: '700', marginTop: spacing.sm }, + username: { fontSize: 14 }, + // Cards + card: { marginHorizontal: spacing.lg, marginTop: spacing.md, borderRadius: borderRadius.xl, padding: spacing.lg }, + cardHeader: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm, marginBottom: spacing.md }, + cardTitle: { fontSize: 15, fontWeight: '700' }, + // Info grid + infoGrid: { flexDirection: 'row', gap: spacing.sm }, + infoBox: { flex: 1, borderRadius: borderRadius.md, padding: spacing.md }, + infoLabel: { fontSize: 11, fontWeight: '600', textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 2 }, + infoValue: { fontSize: 15, fontWeight: '500' }, + // Theme + themePills: { flexDirection: 'row', borderRadius: borderRadius.md, padding: 3 }, + themePill: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 4, paddingVertical: spacing.sm + 2, borderRadius: borderRadius.sm }, + themePillText: { fontSize: 13, fontWeight: '500' }, + // Menu + menuRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.md, paddingVertical: spacing.sm + 2 }, + menuIconCircle: { width: 36, height: 36, borderRadius: 18, justifyContent: 'center', alignItems: 'center' }, + menuLabel: { flex: 1, fontSize: 15, fontWeight: '500' }, + menuDivider: { height: 1, marginVertical: spacing.xs }, + // History + historyRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm, paddingVertical: spacing.sm }, + historyAddress: { flex: 1, fontSize: 14 }, + // Logout + logoutCard: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginHorizontal: spacing.lg, marginTop: spacing.md, padding: spacing.md, borderRadius: borderRadius.xl, borderWidth: 1, gap: spacing.sm }, + logoutText: { fontSize: 15, fontWeight: '600' }, + // Modal shared + modalBackdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.45)', justifyContent: 'flex-end' }, + modalSheet: { borderTopLeftRadius: 20, borderTopRightRadius: 20, maxHeight: '90%' }, + modalHandle: { width: 36, height: 4, borderRadius: 2, alignSelf: 'center', marginTop: spacing.sm, marginBottom: spacing.xs }, + modalHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: spacing.lg, paddingVertical: spacing.md, borderBottomWidth: 1 }, + modalTitle: { fontSize: 17, fontWeight: '700' }, + modalBody: { padding: spacing.lg }, + modalField: { marginBottom: spacing.md }, + modalLabel: { fontSize: 13, fontWeight: '600', marginBottom: spacing.xs }, + modalInputRow: { flexDirection: 'row', alignItems: 'center', borderRadius: borderRadius.md, borderWidth: 1, paddingHorizontal: spacing.md }, + modalInput: { flex: 1, paddingVertical: spacing.md, fontSize: 16 }, + modalFeedback: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm, padding: spacing.md, borderRadius: borderRadius.md, marginBottom: spacing.md, borderWidth: 1 }, + modalFeedbackText: { fontSize: 14, flex: 1 }, + modalActions: { flexDirection: 'row', justifyContent: 'flex-end', gap: spacing.md, marginTop: spacing.md }, + modalCancelBtn: { paddingVertical: spacing.md, paddingHorizontal: spacing.lg, borderRadius: borderRadius.md, borderWidth: 1 }, + modalCancelText: { fontSize: 15, fontWeight: '500' }, + modalSaveBtn: { paddingVertical: spacing.md, paddingHorizontal: spacing.xl, borderRadius: borderRadius.md, minWidth: 100, alignItems: 'center' }, + modalSaveText: { fontSize: 15, fontWeight: '700' }, + // Avatar modal + avatarTabBar: { flexDirection: 'row', borderBottomWidth: 1 }, + avatarTabBtn: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: spacing.xs, paddingVertical: spacing.md, borderBottomWidth: 2, borderBottomColor: 'transparent' }, + avatarTabLabel: { fontSize: 13, fontWeight: '500' }, + avatarGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: spacing.md }, + avatarOption: { width: '30%', aspectRatio: 1 }, + avatarOptionImage: { width: '100%', height: '100%', borderRadius: borderRadius.lg }, + uploadCard: { flexDirection: 'row', alignItems: 'center', padding: spacing.md, borderRadius: borderRadius.lg, gap: spacing.md }, + uploadIconCircle: { width: 48, height: 48, borderRadius: 24, justifyContent: 'center', alignItems: 'center' }, + uploadTitle: { fontSize: 15, fontWeight: '600' }, + uploadSub: { fontSize: 13, marginTop: 1 }, // Addresses - addressLoadingText: { - textAlign: 'center', - fontSize: 14, - paddingVertical: spacing.lg, - }, - addressList: { - gap: spacing.sm, - }, - addressItem: { - flexDirection: 'row', - alignItems: 'flex-start', - justifyContent: 'space-between', - padding: spacing.md, - borderRadius: borderRadius.lg, - borderWidth: 1, - }, - addressItemInfo: { - flex: 1, - gap: 2, - }, - addressLabel: { - fontSize: 13, - fontWeight: '700', - textTransform: 'uppercase', - }, - addressText: { - fontSize: 14, - }, - addressBadge: { - alignSelf: 'flex-start', - borderRadius: borderRadius.full, - paddingHorizontal: spacing.sm, - paddingVertical: 2, - marginTop: 4, - }, - addressBadgeText: { - color: '#ffffff', - fontSize: 11, - fontWeight: '700', - textTransform: 'uppercase', - }, - addressSetDefault: { - fontSize: 12, - fontWeight: '600', - marginTop: 4, - textDecorationLine: 'underline', - }, - addressItemActions: { - flexDirection: 'row', - gap: spacing.xs, - marginLeft: spacing.sm, - }, - addressActionBtn: { - width: 32, - height: 32, - borderRadius: 8, - justifyContent: 'center', - alignItems: 'center', - }, - addressAddBtn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: spacing.sm, - padding: spacing.md, - marginTop: spacing.md, - borderWidth: 2, - borderStyle: 'dashed', - borderRadius: borderRadius.lg, - }, - addressAddBtnText: { - fontSize: 15, - fontWeight: '600', - }, - addressDefaultCheckbox: { - flexDirection: 'row', - alignItems: 'center', - gap: spacing.sm, - marginBottom: spacing.md, - }, - addressDefaultLabel: { - fontSize: 14, - }, + addrCard: { flexDirection: 'row', alignItems: 'flex-start', padding: spacing.md, borderRadius: borderRadius.lg, borderWidth: 1 }, + addrLabel: { fontSize: 12, fontWeight: '700', textTransform: 'uppercase', marginBottom: 2 }, + addrBadge: { fontSize: 12, fontWeight: '700', textTransform: 'uppercase', marginBottom: 2 }, + addrText: { fontSize: 14, lineHeight: 20 }, + addrDefaultLink: { fontSize: 12, fontWeight: '600', marginTop: 4, textDecorationLine: 'underline' }, + addrAction: { width: 32, height: 32, borderRadius: 8, justifyContent: 'center', alignItems: 'center' }, + addAddrBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: spacing.sm, padding: spacing.md, marginTop: spacing.sm, borderWidth: 2, borderStyle: 'dashed', borderRadius: borderRadius.lg }, + addAddrText: { fontSize: 15, fontWeight: '600' }, + checkboxRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm, marginBottom: spacing.md }, + checkboxLabel: { fontSize: 14 }, }); diff --git a/apps/frontend-mobile/app/_layout.tsx b/apps/frontend-mobile/app/_layout.tsx index 7254d59..4394e4a 100644 --- a/apps/frontend-mobile/app/_layout.tsx +++ b/apps/frontend-mobile/app/_layout.tsx @@ -1,7 +1,7 @@ -import { useEffect, useRef } from 'react'; +import React, { useEffect, useRef } from 'react'; import { Stack } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; -import { ImageBackground, StyleSheet, View } from 'react-native'; +import { Image, StyleSheet, View } from 'react-native'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { SafeAreaProvider } from 'react-native-safe-area-context'; @@ -11,6 +11,9 @@ import { ThemeProvider, useThemeContext } from '../components/ThemeProvider'; const queryClient = new QueryClient(); +const bgLight = require('../assets/bg.png'); +const bgDark = require('../assets/bg_dark.png'); + function RootLayoutInner() { const { checkAuth } = useAuthStore(); const { colors, isDark } = useThemeContext(); @@ -19,17 +22,17 @@ function RootLayoutInner() { useEffect(() => { checkAuth(); - + registerForPushNotifications(); - + notificationListener.current = addNotificationListener((notification) => { console.log('Notification received:', notification); }); - + responseListener.current = addNotificationResponseListener((response) => { console.log('Notification clicked:', response); }); - + return () => { notificationListener.current?.remove(); responseListener.current?.remove(); @@ -46,34 +49,34 @@ function RootLayoutInner() { }} > - - - - - @@ -85,21 +88,23 @@ function ThemedBackground({ children }: { children: React.ReactNode }) { const { isDark } = useThemeContext(); return ( - - - {children} - + + + + + {children} + + ); } export default function RootLayout() { return ( - + @@ -113,11 +118,12 @@ export default function RootLayout() { ); } -const bgStyles = StyleSheet.create({ - background: { +const styles = StyleSheet.create({ + root: { flex: 1, }, - backgroundImage: { + bgImage: { + ...StyleSheet.absoluteFillObject, opacity: 0.55, }, overlay: { @@ -127,4 +133,7 @@ const bgStyles = StyleSheet.create({ overlayDark: { backgroundColor: 'rgba(0, 0, 0, 0.25)', }, + content: { + flex: 1, + }, }); diff --git a/apps/frontend-mobile/app/auth/login.tsx b/apps/frontend-mobile/app/auth/login.tsx index aa44c67..a45a96e 100644 --- a/apps/frontend-mobile/app/auth/login.tsx +++ b/apps/frontend-mobile/app/auth/login.tsx @@ -1,40 +1,54 @@ import React, { useState, useEffect } from 'react'; -import { - View, - Text, - TextInput, - TouchableOpacity, - StyleSheet, +import { + View, + Text, + TextInput, + TouchableOpacity, + StyleSheet, KeyboardAvoidingView, Platform, - Alert + Alert, + Image, } from 'react-native'; import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { useAuthStore } from '../../store/authStore'; import { useThemeContext } from '../../components/ThemeProvider'; -import { spacing, borderRadius } from '../../constants/theme'; -import { - isBiometricsAvailable, - authenticateWithBiometrics, - saveBiometricCredentials, - getBiometricUsername +import { register } from '../../services/auth'; +import { spacing, borderRadius, shadows } from '../../constants/theme'; +import { + isBiometricsAvailable, + authenticateWithBiometrics, + saveBiometricCredentials, + getBiometricUsername, } from '../../services/biometrics'; +type Tab = 'login' | 'register'; + export default function LoginScreen() { const router = useRouter(); const { login } = useAuthStore(); const { colors } = useThemeContext(); + const [tab, setTab] = useState('login'); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); const [isLoading, setIsLoading] = useState(false); const [biometricsAvailable, setBiometricsAvailable] = useState(false); const [biometricUsername, setBiometricUsername] = useState(null); + const isRegister = tab === 'register'; + useEffect(() => { checkBiometrics(); }, []); + useEffect(() => { + setUsername(''); + setPassword(''); + setConfirmPassword(''); + }, [tab]); + const checkBiometrics = async () => { try { const available = await isBiometricsAvailable(); @@ -48,21 +62,42 @@ export default function LoginScreen() { } }; - const handleLogin = async () => { - if (!username || !password) { - Alert.alert('Error', 'Por favor ingresa usuario y contraseña'); + const handleSubmit = async () => { + if (!username.trim() || !password) { + Alert.alert('Error', 'Por favor completa todos los campos'); return; } + if (isRegister) { + if (password.length < 8) { + Alert.alert('Error', 'La contraseña debe tener al menos 8 caracteres'); + return; + } + if (password !== confirmPassword) { + Alert.alert('Error', 'Las contraseñas no coinciden'); + return; + } + } + setIsLoading(true); try { - await login(username, password); - if (biometricsAvailable) { - await saveBiometricCredentials(username); + if (isRegister) { + await register(username.trim(), password); + Alert.alert('Éxito', 'Cuenta creada correctamente', [ + { text: 'OK', onPress: () => setTab('login') }, + ]); + } else { + await login(username.trim(), password); + if (biometricsAvailable) { + await saveBiometricCredentials(username.trim()); + } + router.replace('/(tabs)'); } - router.replace('/(tabs)'); } catch (error) { - Alert.alert('Error', 'Credenciales incorrectas'); + Alert.alert( + 'Error', + isRegister ? 'No se pudo crear la cuenta' : 'Credenciales incorrectas' + ); } finally { setIsLoading(false); } @@ -95,61 +130,162 @@ export default function LoginScreen() { style={[styles.container, { backgroundColor: colors.background }]} behavior={Platform.OS === 'ios' ? 'padding' : 'height'} > - - Iniciar Sesión - Ingresa tus credenciales para continuar - - - Usuario - + {/* Logo */} + + + FarmaClic - - Contraseña - - - - - - {isLoading ? 'Ingresando...' : 'Iniciar Sesión'} - - - - {biometricsAvailable && biometricUsername && ( + {/* Tabs */} + setTab('login')} disabled={isLoading} > - - Iniciar con biometría + + Iniciar Sesión + - )} + setTab('register')} + disabled={isLoading} + > + + Crear Cuenta + + + + {/* Card */} + + {/* Header */} + + {isRegister ? 'Crea tu cuenta' : 'Bienvenido de nuevo'} + + + {isRegister + ? 'Guarda tu dirección y recibe notificaciones cuando lleguen medicamentos.' + : 'Inicia sesión para gestionar tu perfil y notificaciones.'} + + + {/* Username */} + + + + + + {/* Password */} + + + + + + {/* Confirm password (register only) */} + {isRegister && ( + + + + + )} + + {/* Hints (register only) */} + {isRegister && ( + + + 3-32 caracteres para el usuario + + + Mínimo 8 caracteres para la contraseña + + + )} + + {/* Submit button */} + + {isLoading ? ( + + ) : ( + + {isRegister ? 'Crear Cuenta' : 'Iniciar Sesión'} + + )} + + + {/* Biometrics (login only) */} + {!isRegister && biometricsAvailable && biometricUsername && ( + + + Iniciar con biometría + + )} + + + {/* Footer link */} router.push('/auth/register')} + onPress={() => setTab(isRegister ? 'login' : 'register')} > - ¿No tienes cuenta? Regístrate + + {isRegister ? '¿Ya tienes cuenta? Inicia sesión' : '¿No tienes cuenta? Regístrate'} + @@ -160,54 +296,102 @@ const styles = StyleSheet.create({ container: { flex: 1, }, - form: { + content: { flex: 1, justifyContent: 'center', - padding: spacing.xl, + paddingHorizontal: spacing.lg, + gap: spacing.lg, }, - title: { - fontSize: 28, - fontWeight: 'bold', - marginBottom: spacing.sm, + logoSection: { + alignItems: 'center', + gap: spacing.sm, }, - subtitle: { - fontSize: 16, - marginBottom: spacing.xl, + logo: { + width: 80, + height: 80, }, - inputContainer: { - marginBottom: spacing.md, + brandName: { + fontSize: 22, + fontWeight: '700', + letterSpacing: -0.3, }, - label: { + tabBar: { + flexDirection: 'row', + borderRadius: borderRadius.lg, + padding: 4, + }, + tab: { + flex: 1, + paddingVertical: spacing.sm + 2, + borderRadius: borderRadius.md, + alignItems: 'center', + }, + tabActive: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + tabText: { fontSize: 14, fontWeight: '500', + }, + tabTextActive: { + fontWeight: '700', + }, + card: { + borderRadius: borderRadius.xl, + padding: spacing.lg, + gap: spacing.sm, + }, + title: { + fontSize: 22, + fontWeight: '700', marginBottom: spacing.xs, }, - input: { + subtitle: { + fontSize: 14, + lineHeight: 20, + marginBottom: spacing.sm, + }, + inputWrapper: { + flexDirection: 'row', + alignItems: 'center', borderRadius: borderRadius.md, - padding: spacing.md, + borderWidth: 1, + paddingHorizontal: spacing.md, + }, + inputIcon: { + marginRight: spacing.sm, + }, + input: { + flex: 1, + paddingVertical: spacing.md, fontSize: 16, }, + hints: { + gap: 2, + marginTop: spacing.xs, + }, + hint: { + fontSize: 12, + lineHeight: 18, + }, button: { - backgroundColor: '#7fbf8f', borderRadius: borderRadius.md, padding: spacing.md, alignItems: 'center', - marginTop: spacing.md, + marginTop: spacing.sm, + flexDirection: 'row', + justifyContent: 'center', }, buttonDisabled: { opacity: 0.6, }, buttonText: { - color: '#ffffff', fontSize: 16, - fontWeight: '600', - }, - linkButton: { - marginTop: spacing.lg, - alignItems: 'center', - }, - linkText: { - fontSize: 14, + fontWeight: '700', }, biometricButton: { flexDirection: 'row', @@ -215,12 +399,20 @@ const styles = StyleSheet.create({ justifyContent: 'center', borderRadius: borderRadius.md, padding: spacing.md, - marginTop: spacing.md, + marginTop: spacing.xs, borderWidth: 1, + gap: spacing.sm, }, biometricText: { - fontSize: 16, + fontSize: 15, + fontWeight: '600', + }, + linkButton: { + alignItems: 'center', + paddingVertical: spacing.sm, + }, + linkText: { + fontSize: 14, fontWeight: '600', - marginLeft: spacing.sm, }, }); diff --git a/apps/frontend-mobile/app/auth/register.tsx b/apps/frontend-mobile/app/auth/register.tsx index 9bd408a..f39fbe4 100644 --- a/apps/frontend-mobile/app/auth/register.tsx +++ b/apps/frontend-mobile/app/auth/register.tsx @@ -1,167 +1,5 @@ -import React, { useState } from 'react'; -import { - View, - Text, - TextInput, - TouchableOpacity, - StyleSheet, - KeyboardAvoidingView, - Platform, - Alert -} from 'react-native'; -import { useRouter } from 'expo-router'; -import { register } from '../../services/auth'; -import { useThemeContext } from '../../components/ThemeProvider'; -import { spacing, borderRadius } from '../../constants/theme'; +import { Redirect } from 'expo-router'; -export default function RegisterScreen() { - const router = useRouter(); - const { colors } = useThemeContext(); - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [isLoading, setIsLoading] = useState(false); - - const handleRegister = async () => { - if (!username || !password || !confirmPassword) { - Alert.alert('Error', 'Por favor completa todos los campos'); - return; - } - - if (password !== confirmPassword) { - Alert.alert('Error', 'Las contraseñas no coinciden'); - return; - } - - setIsLoading(true); - try { - await register(username, password); - router.replace('/(tabs)'); - } catch (error) { - Alert.alert('Error', 'No se pudo crear la cuenta'); - } finally { - setIsLoading(false); - } - }; - - return ( - - - Crear Cuenta - Regístrate para empezar - - - Usuario - - - - - Contraseña - - - - - Confirmar Contraseña - - - - - - {isLoading ? 'Creando cuenta...' : 'Crear Cuenta'} - - - - router.back()} - > - ¿Ya tienes cuenta? Inicia sesión - - - - ); +export default function RegisterRedirect() { + return ; } - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - form: { - flex: 1, - justifyContent: 'center', - padding: spacing.xl, - }, - title: { - fontSize: 28, - fontWeight: 'bold', - marginBottom: spacing.sm, - }, - subtitle: { - fontSize: 16, - marginBottom: spacing.xl, - }, - inputContainer: { - marginBottom: spacing.md, - }, - label: { - fontSize: 14, - fontWeight: '500', - marginBottom: spacing.xs, - }, - input: { - borderRadius: borderRadius.md, - padding: spacing.md, - fontSize: 16, - }, - button: { - backgroundColor: '#7fbf8f', - borderRadius: borderRadius.md, - padding: spacing.md, - alignItems: 'center', - marginTop: spacing.md, - }, - buttonDisabled: { - opacity: 0.6, - }, - buttonText: { - color: '#ffffff', - fontSize: 16, - fontWeight: '600', - }, - linkButton: { - marginTop: spacing.lg, - alignItems: 'center', - }, - linkText: { - fontSize: 14, - }, -}); diff --git a/apps/frontend/src/views/SearchView.css b/apps/frontend/src/views/SearchView.css index 234ceb5..c1d5bfd 100644 --- a/apps/frontend/src/views/SearchView.css +++ b/apps/frontend/src/views/SearchView.css @@ -149,6 +149,10 @@ white-space: nowrap; } +[data-theme="dark"] .recent-tag { + background: #2c3038; +} + .recent-distance { display: flex; align-items: center; diff --git a/eas.json b/eas.json new file mode 100644 index 0000000..6047d39 --- /dev/null +++ b/eas.json @@ -0,0 +1,21 @@ +{ + "cli": { + "version": ">= 20.5.1", + "appVersionSource": "remote" + }, + "build": { + "development": { + "developmentClient": true, + "distribution": "internal" + }, + "preview": { + "distribution": "internal" + }, + "production": { + "autoIncrement": true + } + }, + "submit": { + "production": {} + } +}