Files
FarmaFinder/apps/frontend-mobile/app/(tabs)/profile.tsx
T
2026-07-07 23:40:40 +02:00

1288 lines
40 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Alert, TextInput, ScrollView, Image, ActivityIndicator, Modal, Pressable, useWindowDimensions } from 'react-native';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import { useAuth } from '../../hooks/useAuth';
import { colors, spacing, borderRadius } from '../../constants/theme';
const TABLET_MIN_WIDTH = 768;
const AVATARS = [
require('../../assets/avatars/avatar1.png'),
require('../../assets/avatars/avatar2.png'),
require('../../assets/avatars/avatar3.png'),
require('../../assets/avatars/avatar4.png'),
require('../../assets/avatars/avatar5.png'),
require('../../assets/avatars/avatar6.png'),
];
const COLOR_CIRCLES = [
require('../../assets/avatars/color1.png'),
require('../../assets/avatars/color2.png'),
require('../../assets/avatars/color3.png'),
require('../../assets/avatars/color4.png'),
require('../../assets/avatars/color5.png'),
require('../../assets/avatars/color6.png'),
];
interface SearchHistoryItem {
id: number;
address: string;
latitude: number | null;
longitude: number | null;
created_at: string;
}
interface Address {
id: number;
address: string;
latitude: number | null;
longitude: number | null;
label: string;
is_default: number;
created_at: string;
}
export default function ProfileScreen() {
const router = useRouter();
const { width } = useWindowDimensions();
const isTablet = width >= TABLET_MIN_WIDTH;
const { user, isAuthenticated, isLoading, logout, isAdmin } = useAuth();
const [firstName, setFirstName] = useState(user?.first_name || '');
const [lastName, setLastName] = useState(user?.last_name || '');
const [avatarUrl, setAvatarUrl] = useState(user?.avatar_url || '');
const [avatarLocalSource, setAvatarLocalSource] = useState<number | null>(null);
const [searchHistory, setSearchHistory] = useState<SearchHistoryItem[]>([]);
// 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('');
const [configEmail, setConfigEmail] = useState('');
const [configCity, setConfigCity] = useState('');
const [configAddress, setConfigAddress] = useState('');
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<Address[]>([]);
const [addressesLoading, setAddressesLoading] = useState(false);
const [showAddressForm, setShowAddressForm] = useState(false);
const [editingAddressId, setEditingAddressId] = useState<number | null>(null);
const [formAddress, setFormAddress] = useState('');
const [formLabel, setFormLabel] = useState('');
const [formDefault, setFormDefault] = useState(false);
const [formSaving, setFormSaving] = useState(false);
const [formError, setFormError] = useState('');
useEffect(() => {
if (isAuthenticated) {
loadSearchHistory();
}
}, [isAuthenticated]);
useEffect(() => {
setFirstName(user?.first_name || '');
setLastName(user?.last_name || '');
setAvatarUrl(user?.avatar_url || '');
}, [user]);
async function loadSearchHistory() {
try {
const res = await fetch('/api/search-history', { credentials: 'include' });
if (res.ok) {
const data = await res.json();
setSearchHistory(data);
}
} catch (err) {
console.error('Error loading search history:', err);
}
}
function openConfig() {
setConfigFirstName(user?.first_name || '');
setConfigLastName(user?.last_name || '');
setConfigEmail(user?.email || '');
setConfigCity(user?.city || '');
setConfigAddress(user?.address || '');
setConfigFeedback(null);
setShowConfig(true);
}
async function handleConfigSave() {
setConfigSaving(true);
setConfigFeedback(null);
try {
const res = await fetch('/api/users/me', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
first_name: configFirstName.trim() || null,
last_name: configLastName.trim() || null,
email: configEmail.trim() || null,
city: configCity.trim() || null,
address: configAddress.trim() || null,
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || 'Error al guardar');
}
const updated = await res.json();
setFirstName(updated.first_name || '');
setLastName(updated.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);
}
}
async function handlePickImage() {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1],
quality: 0.8,
});
if (!result.canceled && result.assets[0]) {
setAvatarUrl(result.assets[0].uri);
setShowAvatarModal(false);
try {
const res = await fetch('/api/users/me', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ avatar_url: result.assets[0].uri }),
});
if (res.ok) {
const updated = await res.json();
// Trigger auth refresh to update user state
}
} catch (err) {
console.error('Error saving avatar:', err);
}
}
}
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,
});
if (!result.canceled && result.assets[0]) {
setAvatarUrl(result.assets[0].uri);
setShowAvatarModal(false);
try {
const res = await fetch('/api/users/me', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ avatar_url: result.assets[0].uri }),
});
if (res.ok) {
const updated = await res.json();
}
} catch (err) {
console.error('Error saving avatar:', err);
}
}
}
async function handleSelectPresetAvatar(index: number) {
const avatarSource = AVATARS[index];
setAvatarUrl(avatarSource);
setShowAvatarModal(false);
try {
const res = await fetch('/api/users/me', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ avatar_url: `preset_avatar_${index + 1}` }),
});
if (res.ok) {
const updated = await res.json();
}
} catch (err) {
console.error('Error saving avatar:', err);
}
}
async function handleSelectColor(index: number) {
const colorSource = COLOR_CIRCLES[index];
setAvatarUrl(colorSource);
setShowAvatarModal(false);
try {
const res = await fetch('/api/users/me', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ avatar_url: `color_circle_${index + 1}` }),
});
if (res.ok) {
const updated = await res.json();
}
} catch (err) {
console.error('Error saving avatar:', err);
}
}
async function handleDeleteSearch(id: number) {
try {
const res = await fetch(`/api/search-history/${id}`, {
method: 'DELETE',
credentials: 'include',
});
if (res.ok) {
setSearchHistory(prev => prev.filter(item => item.id !== id));
}
} catch (err) {
console.error('Error deleting search:', err);
}
}
// Addresses
async function loadAddresses() {
setAddressesLoading(true);
try {
const res = await fetch('/api/addresses', { credentials: 'include' });
if (res.ok) {
const data = await res.json();
setAddresses(data);
}
} catch (err) {
console.error('Error loading addresses:', err);
} finally {
setAddressesLoading(false);
}
}
function openAddresses() {
setShowAddresses(true);
loadAddresses();
}
function openAddAddressForm() {
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);
}
async function handleAddressSave() {
const addr = formAddress.trim();
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);
}
}
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);
}
}
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);
}
}
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');
}
},
]
);
};
if (isLoading) {
return (
<View style={styles.container}>
<Text style={styles.loadingText}>Cargando...</Text>
</View>
);
}
if (!isAuthenticated) {
return (
<View style={styles.container}>
<View style={styles.authPrompt}>
<Ionicons name="person-outline" size={isTablet ? 80 : 64} color={colors.textSecondary} />
<Text style={[styles.authTitle, isTablet && styles.authTitleTablet]}>Inicia Sesión</Text>
<Text style={[styles.authSubtitle, isTablet && styles.authSubtitleTablet]}>
Inicia sesión para acceder a todas las funcionalidades
</Text>
<TouchableOpacity
style={[styles.button, isTablet && styles.buttonTablet]}
onPress={() => router.push('/auth/login')}
>
<Text style={[styles.buttonText, isTablet && styles.buttonTextTablet]}>Iniciar Sesión</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.linkButton}
onPress={() => router.push('/auth/register')}
>
<Text style={styles.linkText}>Crear cuenta nueva</Text>
</TouchableOpacity>
</View>
</View>
);
}
return (
<ScrollView style={styles.container}>
{/* Avatar Section */}
<View style={[styles.avatarSection, isTablet && styles.avatarSectionTablet]}>
<TouchableOpacity style={[styles.avatarContainer, isTablet && styles.avatarContainerTablet]} onPress={() => setShowAvatarModal(true)}>
{avatarUrl ? (
<Image source={{ uri: avatarUrl }} style={[styles.avatarImage, isTablet && styles.avatarImageTablet]} />
) : (
<Ionicons name="person" size={isTablet ? 50 : 40} color={colors.primary} />
)}
<View style={styles.avatarOverlay}>
<Ionicons name="camera" size={24} color="white" />
</View>
</TouchableOpacity>
<Text style={styles.editAvatarText}>Toca para cambiar foto</Text>
</View>
{/* Read-only Name Section */}
<View style={[styles.infoSection, isTablet && styles.infoSectionTablet]}>
<View style={styles.infoCard}>
<Text style={styles.infoLabel}>Nombre</Text>
<Text style={styles.infoValue}>{firstName || '—'}</Text>
</View>
<View style={styles.infoCard}>
<Text style={styles.infoLabel}>Apellidos</Text>
<Text style={styles.infoValue}>{lastName || '—'}</Text>
</View>
</View>
{/* Menu Section */}
<View style={[styles.menuSection, isTablet && styles.menuSectionTablet]}>
<TouchableOpacity style={styles.menuItem} onPress={openConfig}>
<Ionicons name="settings" size={24} color={colors.textSecondary} />
<Text style={styles.menuText}>Configuración</Text>
<Ionicons name="chevron-forward" size={20} color={colors.textSecondary} />
</TouchableOpacity>
<TouchableOpacity style={styles.menuItem} onPress={openAddresses}>
<Ionicons name="location" size={24} color={colors.primary} />
<Text style={styles.menuText}>Mis Direcciones</Text>
<Ionicons name="chevron-forward" size={20} color={colors.textSecondary} />
</TouchableOpacity>
{searchHistory.length > 0 && (
<View style={styles.searchHistorySection}>
<Text style={styles.searchHistoryTitle}>Tus búsquedas recientes:</Text>
{searchHistory.map((item) => (
<View key={item.id} style={styles.searchItem}>
<Text style={styles.searchAddress}>{item.address}</Text>
<TouchableOpacity
onPress={() => handleDeleteSearch(item.id)}
style={styles.searchDelete}
>
<Ionicons name="close-circle" size={20} color={colors.textSecondary} />
</TouchableOpacity>
</View>
))}
</View>
)}
{isAdmin && (
<TouchableOpacity style={styles.menuItem}>
<Ionicons name="settings" size={24} color={colors.text} />
<Text style={styles.menuText}>Panel Admin</Text>
<Ionicons name="chevron-forward" size={20} color={colors.textSecondary} />
</TouchableOpacity>
)}
</View>
{/* Logout Button */}
<TouchableOpacity style={[styles.logoutButton, isTablet && styles.logoutButtonTablet]} onPress={handleLogout}>
<Ionicons name="log-out" size={20} color={colors.danger} />
<Text style={styles.logoutText}>Cerrar Sesión</Text>
</TouchableOpacity>
{/* Avatar Selection Modal */}
<Modal visible={showAvatarModal} animationType="slide" transparent>
<View style={styles.modalBackdrop}>
<View style={styles.avatarModalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Cambiar Avatar</Text>
<TouchableOpacity onPress={() => setShowAvatarModal(false)}>
<Ionicons name="close" size={24} color={colors.textSecondary} />
</TouchableOpacity>
</View>
{/* Tab Buttons */}
<View style={styles.tabContainer}>
<TouchableOpacity
style={[styles.tabButton, avatarTab === 'presets' && styles.tabButtonActive]}
onPress={() => setAvatarTab('presets')}
>
<Ionicons name="person" size={20} color={avatarTab === 'presets' ? colors.primary : colors.textSecondary} />
<Text style={[styles.tabText, avatarTab === 'presets' && styles.tabTextActive]}>Prediseñado</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tabButton, avatarTab === 'colors' && styles.tabButtonActive]}
onPress={() => setAvatarTab('colors')}
>
<Ionicons name="color-palette" size={20} color={avatarTab === 'colors' ? colors.primary : colors.textSecondary} />
<Text style={[styles.tabText, avatarTab === 'colors' && styles.tabTextActive]}>Colores</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tabButton, avatarTab === 'upload' && styles.tabButtonActive]}
onPress={() => setAvatarTab('upload')}
>
<Ionicons name="cloud-upload" size={20} color={avatarTab === 'upload' ? colors.primary : colors.textSecondary} />
<Text style={[styles.tabText, avatarTab === 'upload' && styles.tabTextActive]}>Subir</Text>
</TouchableOpacity>
</View>
{/* Tab Content */}
<ScrollView style={styles.tabContent}>
{avatarTab === 'presets' && (
<View style={styles.avatarGrid}>
{AVATARS.map((avatar, index) => (
<TouchableOpacity
key={index}
style={styles.avatarOption}
onPress={() => handleSelectPresetAvatar(index)}
>
<Image source={avatar} style={styles.avatarOptionImage} />
</TouchableOpacity>
))}
</View>
)}
{avatarTab === 'colors' && (
<View style={styles.avatarGrid}>
{COLOR_CIRCLES.map((color, index) => (
<TouchableOpacity
key={index}
style={styles.avatarOption}
onPress={() => handleSelectColor(index)}
>
<Image source={color} style={styles.avatarOptionImage} />
</TouchableOpacity>
))}
</View>
)}
{avatarTab === 'upload' && (
<View style={styles.uploadSection}>
<TouchableOpacity style={styles.uploadOption} onPress={handleTakePhoto}>
<View style={styles.uploadIconContainer}>
<Ionicons name="camera" size={32} color={colors.primary} />
</View>
<Text style={styles.uploadOptionTitle}>Tomar foto</Text>
<Text style={styles.uploadOptionSubtitle}>Usa la cámara de tu dispositivo</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.uploadOption} onPress={handlePickImage}>
<View style={styles.uploadIconContainer}>
<Ionicons name="images" size={32} color={colors.primary} />
</View>
<Text style={styles.uploadOptionTitle}>Elegir de galería</Text>
<Text style={styles.uploadOptionSubtitle}>Selecciona una imagen existente</Text>
</TouchableOpacity>
</View>
)}
</ScrollView>
</View>
</View>
</Modal>
{/* Config Modal */}
<Modal visible={showConfig} animationType="slide" transparent>
<View style={styles.modalBackdrop}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Configuración</Text>
<TouchableOpacity onPress={() => setShowConfig(false)}>
<Ionicons name="close" size={24} color={colors.textSecondary} />
</TouchableOpacity>
</View>
<ScrollView style={styles.modalBody}>
<View style={styles.modalField}>
<Text style={styles.modalLabel}>Nombre</Text>
<TextInput
style={styles.modalInput}
value={configFirstName}
onChangeText={setConfigFirstName}
placeholder="Tu nombre"
editable={!configSaving}
/>
</View>
<View style={styles.modalField}>
<Text style={styles.modalLabel}>Apellidos</Text>
<TextInput
style={styles.modalInput}
value={configLastName}
onChangeText={setConfigLastName}
placeholder="Tus apellidos"
editable={!configSaving}
/>
</View>
<View style={styles.modalField}>
<Text style={styles.modalLabel}>Correo electrónico</Text>
<TextInput
style={styles.modalInput}
value={configEmail}
onChangeText={setConfigEmail}
placeholder="tu@email.com"
keyboardType="email-address"
autoCapitalize="none"
editable={!configSaving}
/>
</View>
<View style={styles.modalField}>
<Text style={styles.modalLabel}>Ciudad</Text>
<TextInput
style={styles.modalInput}
value={configCity}
onChangeText={setConfigCity}
placeholder="Tu ciudad"
editable={!configSaving}
/>
</View>
<View style={styles.modalField}>
<Text style={styles.modalLabel}>Dirección</Text>
<TextInput
style={styles.modalInput}
value={configAddress}
onChangeText={setConfigAddress}
placeholder="Calle Mayor 1, Madrid"
editable={!configSaving}
/>
</View>
{configFeedback && (
<View style={[styles.modalFeedback, configFeedback.type === 'ok' ? styles.modalFeedbackOk : styles.modalFeedbackErr]}>
<Text style={styles.modalFeedbackText}>{configFeedback.text}</Text>
</View>
)}
<View style={styles.modalActions}>
<TouchableOpacity
style={styles.modalCancelBtn}
onPress={() => setShowConfig(false)}
disabled={configSaving}
>
<Text style={styles.modalCancelText}>Cancelar</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.modalSaveBtn, configSaving && styles.modalSaveBtnDisabled]}
onPress={handleConfigSave}
disabled={configSaving}
>
{configSaving ? (
<ActivityIndicator color={colors.textInverse} />
) : (
<Text style={styles.modalSaveText}>Guardar</Text>
)}
</TouchableOpacity>
</View>
</ScrollView>
</View>
</View>
</Modal>
{/* Addresses Modal */}
<Modal visible={showAddresses} animationType="slide" transparent>
<View style={styles.modalBackdrop}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Mis Direcciones</Text>
<TouchableOpacity onPress={() => { setShowAddresses(false); setShowAddressForm(false); }}>
<Ionicons name="close" size={24} color={colors.textSecondary} />
</TouchableOpacity>
</View>
<ScrollView style={styles.modalBody}>
{showAddressForm ? (
<View>
<View style={styles.modalField}>
<Text style={styles.modalLabel}>Dirección</Text>
<TextInput
style={styles.modalInput}
value={formAddress}
onChangeText={setFormAddress}
placeholder="Calle Mayor 1, Madrid"
editable={!formSaving}
/>
</View>
<View style={styles.modalField}>
<Text style={styles.modalLabel}>Etiqueta (opcional)</Text>
<TextInput
style={styles.modalInput}
value={formLabel}
onChangeText={setFormLabel}
placeholder="Ej: Casa, Trabajo, Segunda residencia"
editable={!formSaving}
/>
</View>
<TouchableOpacity
style={styles.addressDefaultCheckbox}
onPress={() => setFormDefault(!formDefault)}
disabled={formSaving}
>
<Ionicons
name={formDefault ? 'checkbox' : 'square-outline'}
size={22}
color={formDefault ? colors.primary : colors.textSecondary}
/>
<Text style={styles.addressDefaultLabel}>Dirección predeterminada</Text>
</TouchableOpacity>
{formError ? (
<View style={[styles.modalFeedback, styles.modalFeedbackErr]}>
<Text style={styles.modalFeedbackText}>{formError}</Text>
</View>
) : null}
<View style={styles.modalActions}>
<TouchableOpacity
style={styles.modalCancelBtn}
onPress={() => { setShowAddressForm(false); setFormError(''); }}
disabled={formSaving}
>
<Text style={styles.modalCancelText}>Cancelar</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.modalSaveBtn, formSaving && styles.modalSaveBtnDisabled]}
onPress={handleAddressSave}
disabled={formSaving}
>
{formSaving ? (
<ActivityIndicator color={colors.textInverse} />
) : (
<Text style={styles.modalSaveText}>{editingAddressId ? 'Actualizar' : 'Añadir'}</Text>
)}
</TouchableOpacity>
</View>
</View>
) : (
<View>
{addressesLoading ? (
<Text style={styles.addressLoadingText}>Cargando direcciones...</Text>
) : (
<View style={styles.addressList}>
{user?.address ? (
<View style={[styles.addressItem, styles.addressItemDefault]}>
<View style={styles.addressItemInfo}>
<Text style={styles.addressLabel}>Dirección principal</Text>
<Text style={styles.addressText}>{user.address}</Text>
<View style={styles.addressBadge}>
<Text style={styles.addressBadgeText}>Predeterminada</Text>
</View>
</View>
</View>
) : null}
{addresses.map((addr) => (
<View key={addr.id} style={styles.addressItem}>
<View style={styles.addressItemInfo}>
{addr.label ? <Text style={styles.addressLabel}>{addr.label}</Text> : null}
<Text style={styles.addressText}>{addr.address}</Text>
<TouchableOpacity onPress={() => handleSetDefault(addr.id)}>
<Text style={styles.addressSetDefault}>Establecer como predeterminada</Text>
</TouchableOpacity>
</View>
<View style={styles.addressItemActions}>
<TouchableOpacity style={styles.addressActionBtn} onPress={() => openEditAddressForm(addr)}>
<Ionicons name="pencil" size={18} color={colors.textSecondary} />
</TouchableOpacity>
<TouchableOpacity style={styles.addressActionBtn} onPress={() => handleDeleteAddress(addr.id)}>
<Ionicons name="close" size={18} color={colors.danger} />
</TouchableOpacity>
</View>
</View>
))}
</View>
)}
<TouchableOpacity style={styles.addressAddBtn} onPress={openAddAddressForm}>
<Ionicons name="add" size={22} color={colors.primary} />
<Text style={styles.addressAddBtnText}>Añadir más</Text>
</TouchableOpacity>
</View>
)}
</ScrollView>
</View>
</View>
</Modal>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
loadingText: {
textAlign: 'center',
marginTop: spacing.xxl,
color: colors.textSecondary,
},
authPrompt: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: spacing.xl,
},
authTitle: {
fontSize: 24,
fontWeight: 'bold',
color: colors.text,
marginTop: spacing.lg,
},
authTitleTablet: {
fontSize: 32,
},
authSubtitle: {
fontSize: 16,
color: colors.textSecondary,
textAlign: 'center',
marginTop: spacing.sm,
marginBottom: spacing.xl,
},
authSubtitleTablet: {
fontSize: 18,
maxWidth: 400,
},
avatarSection: {
alignItems: 'center',
padding: spacing.xl,
backgroundColor: colors.card,
},
avatarSectionTablet: {
paddingVertical: spacing.xxl,
},
avatarContainer: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: colors.primaryContainer,
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,
color: colors.textSecondary,
fontSize: 14,
},
infoSection: {
padding: spacing.lg,
backgroundColor: colors.card,
marginTop: spacing.sm,
gap: spacing.md,
},
infoSectionTablet: {
maxWidth: 600,
alignSelf: 'center',
width: '100%',
},
infoCard: {
backgroundColor: colors.surfaceLow,
padding: spacing.md,
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: colors.border,
},
infoLabel: {
fontSize: 12,
fontWeight: '600',
color: colors.textSecondary,
textTransform: 'uppercase',
letterSpacing: 0.05,
marginBottom: 4,
},
infoValue: {
fontSize: 16,
fontWeight: '500',
color: colors.text,
},
menuSection: {
marginTop: spacing.sm,
backgroundColor: colors.card,
},
menuSectionTablet: {
maxWidth: 600,
alignSelf: 'center',
width: '100%',
},
menuItem: {
flexDirection: 'row',
alignItems: 'center',
padding: spacing.md,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
menuText: {
flex: 1,
marginLeft: spacing.md,
fontSize: 16,
color: colors.text,
},
searchHistorySection: {
padding: spacing.md,
backgroundColor: colors.surfaceLow,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
searchHistoryTitle: {
fontSize: 14,
color: colors.textSecondary,
marginBottom: spacing.sm,
},
searchItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: spacing.sm,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
searchAddress: {
flex: 1,
fontSize: 14,
color: colors.text,
},
searchDelete: {
padding: spacing.xs,
},
button: {
backgroundColor: colors.primary,
borderRadius: borderRadius.lg,
paddingVertical: spacing.md,
paddingHorizontal: spacing.xl,
},
buttonTablet: {
paddingVertical: spacing.lg,
paddingHorizontal: spacing.xl * 1.5,
},
buttonText: {
color: colors.textInverse,
fontSize: 16,
fontWeight: '600',
},
buttonTextTablet: {
fontSize: 18,
},
linkButton: {
marginTop: spacing.md,
},
linkText: {
color: colors.primary,
fontSize: 14,
},
logoutButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
margin: spacing.xl,
padding: spacing.md,
backgroundColor: colors.card,
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: colors.danger,
},
logoutButtonTablet: {
maxWidth: 600,
alignSelf: 'center',
width: '100%',
},
logoutText: {
marginLeft: spacing.sm,
color: colors.danger,
fontSize: 16,
fontWeight: '500',
},
// Modal styles
modalBackdrop: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
modalContent: {
backgroundColor: colors.card,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
maxHeight: '90%',
},
modalHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: spacing.lg,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
modalTitle: {
fontSize: 18,
fontWeight: 'bold',
color: colors.text,
},
modalBody: {
padding: spacing.lg,
},
modalField: {
marginBottom: spacing.md,
},
modalLabel: {
fontSize: 13,
fontWeight: '600',
color: colors.textSecondary,
marginBottom: spacing.xs,
},
modalInput: {
borderWidth: 1,
borderColor: colors.border,
borderRadius: borderRadius.lg,
padding: spacing.md,
fontSize: 16,
color: colors.text,
},
modalFeedback: {
padding: spacing.md,
borderRadius: borderRadius.lg,
marginBottom: spacing.md,
},
modalFeedbackOk: {
backgroundColor: '#eaf7ec',
borderWidth: 1,
borderColor: '#cfead0',
},
modalFeedbackErr: {
backgroundColor: colors.dangerContainer,
borderWidth: 1,
borderColor: '#fecaca',
},
modalFeedbackText: {
fontSize: 14,
color: colors.text,
},
modalActions: {
flexDirection: 'row',
justifyContent: 'flex-end',
gap: spacing.md,
marginTop: spacing.md,
},
modalCancelBtn: {
paddingVertical: spacing.md,
paddingHorizontal: spacing.lg,
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: colors.border,
},
modalCancelText: {
fontSize: 16,
color: colors.text,
},
modalSaveBtn: {
backgroundColor: colors.primary,
paddingVertical: spacing.md,
paddingHorizontal: spacing.xl,
borderRadius: borderRadius.lg,
minWidth: 100,
alignItems: 'center',
},
modalSaveBtnDisabled: {
opacity: 0.6,
},
modalSaveText: {
color: colors.textInverse,
fontSize: 16,
fontWeight: '600',
},
// Avatar Modal styles
avatarModalContent: {
backgroundColor: colors.card,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
maxHeight: '80%',
},
tabContainer: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
tabButton: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.md,
gap: spacing.xs,
},
tabButtonActive: {
borderBottomWidth: 2,
borderBottomColor: colors.primary,
},
tabText: {
fontSize: 14,
color: colors.textSecondary,
},
tabTextActive: {
color: colors.primary,
fontWeight: '600',
},
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,
backgroundColor: colors.surfaceLow,
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: colors.border,
},
uploadIconContainer: {
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: colors.card,
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
uploadOptionTitle: {
fontSize: 16,
fontWeight: '600',
color: colors.text,
flex: 1,
},
uploadOptionSubtitle: {
fontSize: 13,
color: colors.textSecondary,
flex: 1,
},
// Addresses
addressLoadingText: {
textAlign: 'center',
color: colors.textSecondary,
fontSize: 14,
paddingVertical: spacing.lg,
},
addressList: {
gap: spacing.sm,
},
addressItem: {
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
padding: spacing.md,
backgroundColor: colors.surfaceLow,
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: colors.border,
},
addressItemDefault: {
borderColor: colors.primary,
backgroundColor: '#eaf7ec',
},
addressItemInfo: {
flex: 1,
gap: 2,
},
addressLabel: {
fontSize: 13,
fontWeight: '700',
color: colors.primary,
textTransform: 'uppercase',
},
addressText: {
fontSize: 14,
color: colors.text,
},
addressBadge: {
alignSelf: 'flex-start',
backgroundColor: colors.primary,
borderRadius: borderRadius.full,
paddingHorizontal: spacing.sm,
paddingVertical: 2,
marginTop: 4,
},
addressBadgeText: {
color: colors.textInverse,
fontSize: 11,
fontWeight: '700',
textTransform: 'uppercase',
},
addressSetDefault: {
fontSize: 12,
fontWeight: '600',
color: colors.primary,
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,
borderColor: colors.border,
borderStyle: 'dashed',
borderRadius: borderRadius.lg,
},
addressAddBtnText: {
fontSize: 15,
fontWeight: '600',
color: colors.primary,
},
addressDefaultCheckbox: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
marginBottom: spacing.md,
},
addressDefaultLabel: {
fontSize: 14,
color: colors.text,
},
});