1305 lines
45 KiB
TypeScript
1305 lines
45 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 { useThemeContext } from '../../components/ThemeProvider';
|
|
import { useThemeStore, ThemeMode } from '../../store/themeStore';
|
|
import { spacing, borderRadius } from '../../constants/theme';
|
|
import api from '../../services/api';
|
|
|
|
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'),
|
|
];
|
|
|
|
function resolveAvatarUrl(url: string | null | undefined): number | null {
|
|
if (!url) return null;
|
|
const matchPreset = url.match(/^preset_avatar_(\d+)$/);
|
|
if (matchPreset) {
|
|
const idx = parseInt(matchPreset[1], 10);
|
|
if (idx >= 1 && idx <= 6) return AVATARS[idx - 1];
|
|
}
|
|
const matchColor = url.match(/^color_circle_(\d+)$/);
|
|
if (matchColor) {
|
|
const idx = parseInt(matchColor[1], 10);
|
|
if (idx >= 1 && idx <= 6) return COLOR_CIRCLES[idx - 1];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
const THEME_OPTIONS: { mode: ThemeMode; label: string; icon: string }[] = [
|
|
{ mode: 'system', label: 'Sistema', icon: 'phone-portrait-outline' },
|
|
{ mode: 'light', label: 'Claro', icon: 'sunny-outline' },
|
|
{ mode: 'dark', label: 'Oscuro', icon: 'moon-outline' },
|
|
];
|
|
|
|
export default function ProfileScreen() {
|
|
const router = useRouter();
|
|
const { width } = useWindowDimensions();
|
|
const isTablet = width >= TABLET_MIN_WIDTH;
|
|
const { user, isAuthenticated, isLoading, logout, isAdmin } = useAuth();
|
|
const { colors, isDark } = useThemeContext();
|
|
const themeMode = useThemeStore((s) => s.mode);
|
|
const setThemeMode = useThemeStore((s) => s.setMode);
|
|
|
|
const initialResolved = resolveAvatarUrl(user?.avatar_url);
|
|
const [firstName, setFirstName] = useState(user?.first_name || '');
|
|
const [lastName, setLastName] = useState(user?.last_name || '');
|
|
const [avatarUrl, setAvatarUrl] = useState(initialResolved ? '' : (user?.avatar_url || ''));
|
|
const [avatarLocalSource, setAvatarLocalSource] = useState<number | null>(initialResolved);
|
|
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 || '');
|
|
const resolved = resolveAvatarUrl(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);
|
|
}
|
|
}
|
|
|
|
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 api.put('/users/me', {
|
|
first_name: configFirstName.trim() || null,
|
|
last_name: configLastName.trim() || null,
|
|
email: configEmail.trim() || null,
|
|
city: configCity.trim() || null,
|
|
address: configAddress.trim() || null,
|
|
});
|
|
const updated = res.data;
|
|
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,
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
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');
|
|
}
|
|
},
|
|
]
|
|
);
|
|
};
|
|
|
|
const getThemeModeLabel = () => {
|
|
const opt = THEME_OPTIONS.find(o => o.mode === themeMode);
|
|
return opt?.label || 'Sistema';
|
|
};
|
|
|
|
const getThemeModeIcon = (): 'phone-portrait-outline' | 'sunny-outline' | 'moon-outline' => {
|
|
if (themeMode === 'light') return 'sunny-outline';
|
|
if (themeMode === 'dark') return 'moon-outline';
|
|
return 'phone-portrait-outline';
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
|
<Text style={[styles.loadingText, { color: colors.textSecondary }]}>Cargando...</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (!isAuthenticated) {
|
|
return (
|
|
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
|
<View style={styles.authPrompt}>
|
|
<Ionicons name="person-outline" size={isTablet ? 80 : 64} color={colors.textSecondary} />
|
|
<Text style={[styles.authTitle, isTablet && styles.authTitleTablet, { color: colors.text }]}>Inicia Sesión</Text>
|
|
<Text style={[styles.authSubtitle, isTablet && styles.authSubtitleTablet, { color: colors.textSecondary }]}>
|
|
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, { color: colors.primary }]}>Crear cuenta nueva</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ScrollView style={[styles.container, { backgroundColor: colors.background }]}>
|
|
{/* Avatar Section */}
|
|
<View style={[styles.avatarSection, isTablet && styles.avatarSectionTablet, { backgroundColor: colors.card }]}>
|
|
<TouchableOpacity style={[styles.avatarContainer, isTablet && styles.avatarContainerTablet, { backgroundColor: colors.primaryContainer }]} onPress={() => setShowAvatarModal(true)}>
|
|
{avatarLocalSource ? (
|
|
<Image source={avatarLocalSource} style={[styles.avatarImage, isTablet && styles.avatarImageTablet]} />
|
|
) : 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, { color: colors.textSecondary }]}>Toca para cambiar foto</Text>
|
|
</View>
|
|
|
|
{/* Read-only Name Section */}
|
|
<View style={[styles.infoSection, isTablet && styles.infoSectionTablet, { backgroundColor: colors.card }]}>
|
|
<View style={[styles.infoCard, { backgroundColor: colors.surfaceLow, borderColor: colors.border }]}>
|
|
<Text style={[styles.infoLabel, { color: colors.textSecondary }]}>Nombre</Text>
|
|
<Text style={[styles.infoValue, { color: colors.text }]}>{firstName || '—'}</Text>
|
|
</View>
|
|
<View style={[styles.infoCard, { backgroundColor: colors.surfaceLow, borderColor: colors.border }]}>
|
|
<Text style={[styles.infoLabel, { color: colors.textSecondary }]}>Apellidos</Text>
|
|
<Text style={[styles.infoValue, { color: colors.text }]}>{lastName || '—'}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Menu Section */}
|
|
<View style={[styles.menuSection, isTablet && styles.menuSectionTablet, { backgroundColor: colors.card }]}>
|
|
{/* Theme Toggle */}
|
|
<View style={[styles.themeSection, { borderBottomColor: colors.border }]}>
|
|
<View style={styles.themeHeader}>
|
|
<Ionicons name={getThemeModeIcon()} size={24} color={colors.primary} />
|
|
<Text style={[styles.themeTitle, { color: colors.text }]}>Modo de pantalla</Text>
|
|
</View>
|
|
<View style={styles.themeOptions}>
|
|
{THEME_OPTIONS.map((opt) => (
|
|
<TouchableOpacity
|
|
key={opt.mode}
|
|
style={[
|
|
styles.themeOption,
|
|
{ backgroundColor: colors.surfaceLow, borderColor: colors.border },
|
|
themeMode === opt.mode && { backgroundColor: colors.primaryContainer, borderColor: colors.primary },
|
|
]}
|
|
onPress={() => setThemeMode(opt.mode)}
|
|
>
|
|
<Ionicons
|
|
name={opt.icon as any}
|
|
size={18}
|
|
color={themeMode === opt.mode ? colors.primary : colors.textSecondary}
|
|
/>
|
|
<Text
|
|
style={[
|
|
styles.themeOptionText,
|
|
{ color: themeMode === opt.mode ? colors.primary : colors.textSecondary },
|
|
themeMode === opt.mode && styles.themeOptionTextActive,
|
|
]}
|
|
>
|
|
{opt.label}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
</View>
|
|
|
|
<TouchableOpacity style={[styles.menuItem, { borderBottomColor: colors.border }]} onPress={openConfig}>
|
|
<Ionicons name="settings" size={24} color={colors.textSecondary} />
|
|
<Text style={[styles.menuText, { color: colors.text }]}>Configuración</Text>
|
|
<Ionicons name="chevron-forward" size={20} color={colors.textSecondary} />
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity style={[styles.menuItem, { borderBottomColor: colors.border }]} onPress={openAddresses}>
|
|
<Ionicons name="location" size={24} color={colors.primary} />
|
|
<Text style={[styles.menuText, { color: colors.text }]}>Mis Direcciones</Text>
|
|
<Ionicons name="chevron-forward" size={20} color={colors.textSecondary} />
|
|
</TouchableOpacity>
|
|
|
|
{searchHistory.length > 0 && (
|
|
<View style={[styles.searchHistorySection, { backgroundColor: colors.surfaceLow, borderBottomColor: colors.border }]}>
|
|
<Text style={[styles.searchHistoryTitle, { color: colors.textSecondary }]}>Tus búsquedas recientes:</Text>
|
|
{searchHistory.map((item) => (
|
|
<View key={item.id} style={[styles.searchItem, { borderBottomColor: colors.border }]}>
|
|
<Text style={[styles.searchAddress, { color: colors.text }]}>{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, { color: colors.text }]}>Panel Admin</Text>
|
|
<Ionicons name="chevron-forward" size={20} color={colors.textSecondary} />
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
|
|
{/* Logout Button */}
|
|
<TouchableOpacity style={[styles.logoutButton, isTablet && styles.logoutButtonTablet, { backgroundColor: colors.card, borderColor: colors.danger }]} onPress={handleLogout}>
|
|
<Ionicons name="log-out" size={20} color={colors.danger} />
|
|
<Text style={[styles.logoutText, { color: colors.danger }]}>Cerrar Sesión</Text>
|
|
</TouchableOpacity>
|
|
|
|
{/* Avatar Selection Modal */}
|
|
<Modal visible={showAvatarModal} animationType="slide" transparent>
|
|
<View style={styles.modalBackdrop}>
|
|
<View style={[styles.avatarModalContent, { backgroundColor: colors.card }]}>
|
|
<View style={[styles.modalHeader, { borderBottomColor: colors.border }]}>
|
|
<Text style={[styles.modalTitle, { color: colors.text }]}>Cambiar Avatar</Text>
|
|
<TouchableOpacity onPress={() => setShowAvatarModal(false)}>
|
|
<Ionicons name="close" size={24} color={colors.textSecondary} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Tab Buttons */}
|
|
<View style={[styles.tabContainer, { borderBottomColor: colors.border }]}>
|
|
<TouchableOpacity
|
|
style={[styles.tabButton, avatarTab === 'presets' && { borderBottomWidth: 2, borderBottomColor: colors.primary }]}
|
|
onPress={() => setAvatarTab('presets')}
|
|
>
|
|
<Ionicons name="person" size={20} color={avatarTab === 'presets' ? colors.primary : colors.textSecondary} />
|
|
<Text style={[styles.tabText, avatarTab === 'presets' && { color: colors.primary, fontWeight: '600' }, { color: colors.textSecondary }]}>Prediseñado</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[styles.tabButton, avatarTab === 'colors' && { borderBottomWidth: 2, borderBottomColor: colors.primary }]}
|
|
onPress={() => setAvatarTab('colors')}
|
|
>
|
|
<Ionicons name="color-palette" size={20} color={avatarTab === 'colors' ? colors.primary : colors.textSecondary} />
|
|
<Text style={[styles.tabText, avatarTab === 'colors' && { color: colors.primary, fontWeight: '600' }, { color: colors.textSecondary }]}>Colores</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[styles.tabButton, avatarTab === 'upload' && { borderBottomWidth: 2, borderBottomColor: colors.primary }]}
|
|
onPress={() => setAvatarTab('upload')}
|
|
>
|
|
<Ionicons name="cloud-upload" size={20} color={avatarTab === 'upload' ? colors.primary : colors.textSecondary} />
|
|
<Text style={[styles.tabText, avatarTab === 'upload' && { color: colors.primary, fontWeight: '600' }, { color: colors.textSecondary }]}>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, { backgroundColor: colors.surfaceLow, borderColor: colors.border }]} onPress={handleTakePhoto}>
|
|
<View style={[styles.uploadIconContainer, { backgroundColor: colors.card }]}>
|
|
<Ionicons name="camera" size={32} color={colors.primary} />
|
|
</View>
|
|
<Text style={[styles.uploadOptionTitle, { color: colors.text }]}>Tomar foto</Text>
|
|
<Text style={[styles.uploadOptionSubtitle, { color: colors.textSecondary }]}>Usa la cámara de tu dispositivo</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity style={[styles.uploadOption, { backgroundColor: colors.surfaceLow, borderColor: colors.border }]} onPress={handlePickImage}>
|
|
<View style={[styles.uploadIconContainer, { backgroundColor: colors.card }]}>
|
|
<Ionicons name="images" size={32} color={colors.primary} />
|
|
</View>
|
|
<Text style={[styles.uploadOptionTitle, { color: colors.text }]}>Elegir de galería</Text>
|
|
<Text style={[styles.uploadOptionSubtitle, { color: colors.textSecondary }]}>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, { backgroundColor: colors.card }]}>
|
|
<View style={[styles.modalHeader, { borderBottomColor: colors.border }]}>
|
|
<Text style={[styles.modalTitle, { color: colors.text }]}>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, { color: colors.textSecondary }]}>Nombre</Text>
|
|
<TextInput
|
|
style={[styles.modalInput, { borderColor: colors.border, color: colors.text }]}
|
|
value={configFirstName}
|
|
onChangeText={setConfigFirstName}
|
|
placeholder="Tu nombre"
|
|
placeholderTextColor={colors.textSecondary}
|
|
editable={!configSaving}
|
|
/>
|
|
</View>
|
|
<View style={styles.modalField}>
|
|
<Text style={[styles.modalLabel, { color: colors.textSecondary }]}>Apellidos</Text>
|
|
<TextInput
|
|
style={[styles.modalInput, { borderColor: colors.border, color: colors.text }]}
|
|
value={configLastName}
|
|
onChangeText={setConfigLastName}
|
|
placeholder="Tus apellidos"
|
|
placeholderTextColor={colors.textSecondary}
|
|
editable={!configSaving}
|
|
/>
|
|
</View>
|
|
<View style={styles.modalField}>
|
|
<Text style={[styles.modalLabel, { color: colors.textSecondary }]}>Correo electrónico</Text>
|
|
<TextInput
|
|
style={[styles.modalInput, { borderColor: colors.border, color: colors.text }]}
|
|
value={configEmail}
|
|
onChangeText={setConfigEmail}
|
|
placeholder="tu@email.com"
|
|
placeholderTextColor={colors.textSecondary}
|
|
keyboardType="email-address"
|
|
autoCapitalize="none"
|
|
editable={!configSaving}
|
|
/>
|
|
</View>
|
|
<View style={styles.modalField}>
|
|
<Text style={[styles.modalLabel, { color: colors.textSecondary }]}>Ciudad</Text>
|
|
<TextInput
|
|
style={[styles.modalInput, { borderColor: colors.border, color: colors.text }]}
|
|
value={configCity}
|
|
onChangeText={setConfigCity}
|
|
placeholder="Tu ciudad"
|
|
placeholderTextColor={colors.textSecondary}
|
|
editable={!configSaving}
|
|
/>
|
|
</View>
|
|
<View style={styles.modalField}>
|
|
<Text style={[styles.modalLabel, { color: colors.textSecondary }]}>Dirección</Text>
|
|
<TextInput
|
|
style={[styles.modalInput, { borderColor: colors.border, color: colors.text }]}
|
|
value={configAddress}
|
|
onChangeText={setConfigAddress}
|
|
placeholder="Calle Mayor 1, Madrid"
|
|
placeholderTextColor={colors.textSecondary}
|
|
editable={!configSaving}
|
|
/>
|
|
</View>
|
|
|
|
{configFeedback && (
|
|
<View style={[styles.modalFeedback, configFeedback.type === 'ok' ? { backgroundColor: isDark ? '#1a3a1c' : '#eaf7ec', borderColor: isDark ? '#2a5a35' : '#cfead0' } : { backgroundColor: colors.dangerContainer, borderColor: isDark ? '#5a2020' : '#fecaca' }]}>
|
|
<Text style={[styles.modalFeedbackText, { color: colors.text }]}>{configFeedback.text}</Text>
|
|
</View>
|
|
)}
|
|
|
|
<View style={styles.modalActions}>
|
|
<TouchableOpacity
|
|
style={[styles.modalCancelBtn, { borderColor: colors.border }]}
|
|
onPress={() => setShowConfig(false)}
|
|
disabled={configSaving}
|
|
>
|
|
<Text style={[styles.modalCancelText, { color: colors.text }]}>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, { backgroundColor: colors.card }]}>
|
|
<View style={[styles.modalHeader, { borderBottomColor: colors.border }]}>
|
|
<Text style={[styles.modalTitle, { color: colors.text }]}>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, { color: colors.textSecondary }]}>Dirección</Text>
|
|
<TextInput
|
|
style={[styles.modalInput, { borderColor: colors.border, color: colors.text }]}
|
|
value={formAddress}
|
|
onChangeText={setFormAddress}
|
|
placeholder="Calle Mayor 1, Madrid"
|
|
placeholderTextColor={colors.textSecondary}
|
|
editable={!formSaving}
|
|
/>
|
|
</View>
|
|
<View style={styles.modalField}>
|
|
<Text style={[styles.modalLabel, { color: colors.textSecondary }]}>Etiqueta (opcional)</Text>
|
|
<TextInput
|
|
style={[styles.modalInput, { borderColor: colors.border, color: colors.text }]}
|
|
value={formLabel}
|
|
onChangeText={setFormLabel}
|
|
placeholder="Ej: Casa, Trabajo, Segunda residencia"
|
|
placeholderTextColor={colors.textSecondary}
|
|
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, { color: colors.text }]}>Dirección predeterminada</Text>
|
|
</TouchableOpacity>
|
|
{formError ? (
|
|
<View style={[styles.modalFeedback, { backgroundColor: colors.dangerContainer, borderColor: isDark ? '#5a2020' : '#fecaca' }]}>
|
|
<Text style={[styles.modalFeedbackText, { color: colors.text }]}>{formError}</Text>
|
|
</View>
|
|
) : null}
|
|
<View style={styles.modalActions}>
|
|
<TouchableOpacity
|
|
style={[styles.modalCancelBtn, { borderColor: colors.border }]}
|
|
onPress={() => { setShowAddressForm(false); setFormError(''); }}
|
|
disabled={formSaving}
|
|
>
|
|
<Text style={[styles.modalCancelText, { color: colors.text }]}>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, { color: colors.textSecondary }]}>Cargando direcciones...</Text>
|
|
) : (
|
|
<View style={styles.addressList}>
|
|
{user?.address ? (
|
|
<View style={[styles.addressItem, { borderColor: colors.primary, backgroundColor: isDark ? '#1a3a1c' : '#eaf7ec' }]}>
|
|
<View style={styles.addressItemInfo}>
|
|
<Text style={[styles.addressLabel, { color: colors.primary }]}>Dirección principal</Text>
|
|
<Text style={[styles.addressText, { color: colors.text }]}>{user.address}</Text>
|
|
<View style={[styles.addressBadge, { backgroundColor: colors.primary }]}>
|
|
<Text style={styles.addressBadgeText}>Predeterminada</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
) : null}
|
|
{addresses.map((addr) => (
|
|
<View key={addr.id} style={[styles.addressItem, { backgroundColor: colors.surfaceLow, borderColor: colors.border }]}>
|
|
<View style={styles.addressItemInfo}>
|
|
{addr.label ? <Text style={[styles.addressLabel, { color: colors.primary }]}>{addr.label}</Text> : null}
|
|
<Text style={[styles.addressText, { color: colors.text }]}>{addr.address}</Text>
|
|
<TouchableOpacity onPress={() => handleSetDefault(addr.id)}>
|
|
<Text style={[styles.addressSetDefault, { color: colors.primary }]}>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, { borderColor: colors.border }]} onPress={openAddAddressForm}>
|
|
<Ionicons name="add" size={22} color={colors.primary} />
|
|
<Text style={[styles.addressAddBtnText, { color: colors.primary }]}>Añadir más</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
</ScrollView>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
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,
|
|
},
|
|
// 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,
|
|
},
|
|
});
|