feat: avatar popup options, profile redesign, and search dosage parsing
- Added avatar selection popup with predefined avatar options - Redesigned ProfileView (web + mobile) with avatar upload and editable fields - Added AvatarSelectionModal for mobile profile - Fixed medicine search: parse dosage terms (e.g. 'Paracetamol 1G') to query CIMA only by name and filter by dosage field - Updated styles for profile, search, and medicine results
This commit is contained in:
@@ -1,11 +1,29 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Alert, TextInput, ScrollView, Image, ActivityIndicator, Modal } from 'react-native';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Alert, TextInput, ScrollView, Image, ActivityIndicator, Modal, Pressable } 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 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;
|
||||
@@ -14,6 +32,16 @@ interface SearchHistoryItem {
|
||||
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 { user, isAuthenticated, isLoading, logout, isAdmin } = useAuth();
|
||||
@@ -21,8 +49,13 @@ export default function ProfileScreen() {
|
||||
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('');
|
||||
@@ -33,6 +66,18 @@ 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<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();
|
||||
@@ -109,6 +154,7 @@ export default function ProfileScreen() {
|
||||
|
||||
if (!result.canceled && result.assets[0]) {
|
||||
setAvatarUrl(result.assets[0].uri);
|
||||
setShowAvatarModal(false);
|
||||
try {
|
||||
const res = await fetch('/api/users/me', {
|
||||
method: 'PUT',
|
||||
@@ -126,6 +172,76 @@ export default function ProfileScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
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}`, {
|
||||
@@ -140,6 +256,108 @@ export default function ProfileScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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',
|
||||
@@ -196,7 +414,7 @@ export default function ProfileScreen() {
|
||||
<ScrollView style={styles.container}>
|
||||
{/* Avatar Section */}
|
||||
<View style={styles.avatarSection}>
|
||||
<TouchableOpacity style={styles.avatarContainer} onPress={handlePickImage}>
|
||||
<TouchableOpacity style={styles.avatarContainer} onPress={() => setShowAvatarModal(true)}>
|
||||
{avatarUrl ? (
|
||||
<Image source={{ uri: avatarUrl }} style={styles.avatarImage} />
|
||||
) : (
|
||||
@@ -229,11 +447,11 @@ export default function ProfileScreen() {
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.menuItem}>
|
||||
<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} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{searchHistory.length > 0 && (
|
||||
<View style={styles.searchHistorySection}>
|
||||
@@ -267,6 +485,95 @@ export default function ProfileScreen() {
|
||||
<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}>
|
||||
@@ -361,6 +668,126 @@ export default function ProfileScreen() {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -631,4 +1058,184 @@ const styles = StyleSheet.create({
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
// Avatar Modal styles
|
||||
avatarModalContent: {
|
||||
backgroundColor: colors.card,
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
maxHeight: '80%',
|
||||
},
|
||||
tabContainer: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.separator,
|
||||
},
|
||||
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.background,
|
||||
borderRadius: borderRadius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border || '#E5E5EA',
|
||||
},
|
||||
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.background,
|
||||
borderRadius: borderRadius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border || '#E0E0E0',
|
||||
},
|
||||
addressItemDefault: {
|
||||
borderColor: colors.primary,
|
||||
backgroundColor: 'rgba(0, 69, 13, 0.03)',
|
||||
},
|
||||
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: 12,
|
||||
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 || '#E0E0E0',
|
||||
borderStyle: 'dashed',
|
||||
borderRadius: borderRadius.md,
|
||||
},
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user