feature/profile-redesign #12
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Alert, TextInput, ScrollView, Image, ActivityIndicator } from 'react-native';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Alert, TextInput, ScrollView, Image, ActivityIndicator, Modal } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
@@ -22,8 +22,16 @@ export default function ProfileScreen() {
|
||||
const [lastName, setLastName] = useState(user?.last_name || '');
|
||||
const [avatarUrl, setAvatarUrl] = useState(user?.avatar_url || '');
|
||||
const [searchHistory, setSearchHistory] = useState<SearchHistoryItem[]>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [feedback, setFeedback] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
|
||||
|
||||
// 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);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
@@ -49,29 +57,45 @@ export default function ProfileScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
setSaving(true);
|
||||
setFeedback(null);
|
||||
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: firstName.trim() || null,
|
||||
last_name: lastName.trim() || null,
|
||||
avatar_url: avatarUrl || null,
|
||||
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');
|
||||
}
|
||||
setFeedback({ type: 'ok', text: 'Perfil guardado.' });
|
||||
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) {
|
||||
setFeedback({ type: 'err', text: err.message || 'Error al guardar' });
|
||||
setConfigFeedback({ type: 'err', text: err.message || 'Error al guardar' });
|
||||
} finally {
|
||||
setSaving(false);
|
||||
setConfigSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,10 +117,11 @@ export default function ProfileScreen() {
|
||||
body: JSON.stringify({ avatar_url: result.assets[0].uri }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setFeedback({ type: 'ok', text: 'Foto de perfil actualizada.' });
|
||||
const updated = await res.json();
|
||||
// Trigger auth refresh to update user state
|
||||
}
|
||||
} catch (err) {
|
||||
setFeedback({ type: 'err', text: 'Error al guardar la foto' });
|
||||
console.error('Error saving avatar:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,50 +209,26 @@ export default function ProfileScreen() {
|
||||
<Text style={styles.editAvatarText}>Toca para cambiar foto</Text>
|
||||
</View>
|
||||
|
||||
{/* Editable Fields */}
|
||||
<View style={styles.formSection}>
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Nombre</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={firstName}
|
||||
onChangeText={setFirstName}
|
||||
placeholder="Tu nombre"
|
||||
editable={!saving}
|
||||
/>
|
||||
{/* Read-only Name Section */}
|
||||
<View style={styles.infoSection}>
|
||||
<View style={styles.infoCard}>
|
||||
<Text style={styles.infoLabel}>Nombre</Text>
|
||||
<Text style={styles.infoValue}>{firstName || '—'}</Text>
|
||||
</View>
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Apellidos</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={lastName}
|
||||
onChangeText={setLastName}
|
||||
placeholder="Tus apellidos"
|
||||
editable={!saving}
|
||||
/>
|
||||
<View style={styles.infoCard}>
|
||||
<Text style={styles.infoLabel}>Apellidos</Text>
|
||||
<Text style={styles.infoValue}>{lastName || '—'}</Text>
|
||||
</View>
|
||||
|
||||
{feedback && (
|
||||
<View style={[styles.feedback, feedback.type === 'ok' ? styles.feedbackOk : styles.feedbackErr]}>
|
||||
<Text style={styles.feedbackText}>{feedback.text}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.saveButton, saving && styles.saveButtonDisabled]}
|
||||
onPress={handleSave}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? (
|
||||
<ActivityIndicator color={colors.textInverse} />
|
||||
) : (
|
||||
<Text style={styles.saveButtonText}>Guardar Cambios</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Menu Section */}
|
||||
<View style={styles.menuSection}>
|
||||
<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>
|
||||
|
||||
<View style={styles.menuItem}>
|
||||
<Ionicons name="location" size={24} color={colors.primary} />
|
||||
<Text style={styles.menuText}>Mis Direcciones</Text>
|
||||
@@ -265,6 +266,101 @@ export default function ProfileScreen() {
|
||||
<Ionicons name="log-out" size={20} color={colors.danger} />
|
||||
<Text style={styles.logoutText}>Cerrar Sesión</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 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>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
@@ -333,59 +429,31 @@ const styles = StyleSheet.create({
|
||||
color: colors.textSecondary,
|
||||
fontSize: 14,
|
||||
},
|
||||
formSection: {
|
||||
infoSection: {
|
||||
padding: spacing.xl,
|
||||
backgroundColor: colors.card,
|
||||
marginTop: spacing.md,
|
||||
gap: spacing.md,
|
||||
},
|
||||
inputGroup: {
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: colors.textSecondary,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
input: {
|
||||
infoCard: {
|
||||
backgroundColor: colors.background,
|
||||
padding: spacing.md,
|
||||
borderRadius: borderRadius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border || '#E0E0E0',
|
||||
borderRadius: borderRadius.md,
|
||||
padding: spacing.md,
|
||||
fontSize: 16,
|
||||
color: colors.text,
|
||||
},
|
||||
feedback: {
|
||||
padding: spacing.md,
|
||||
borderRadius: borderRadius.md,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
feedbackOk: {
|
||||
backgroundColor: 'rgba(0, 69, 13, 0.1)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(0, 69, 13, 0.2)',
|
||||
},
|
||||
feedbackErr: {
|
||||
backgroundColor: 'rgba(186, 26, 26, 0.1)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(186, 26, 26, 0.2)',
|
||||
},
|
||||
feedbackText: {
|
||||
fontSize: 14,
|
||||
},
|
||||
saveButton: {
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: borderRadius.md,
|
||||
padding: spacing.md,
|
||||
alignItems: 'center',
|
||||
},
|
||||
saveButtonDisabled: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
saveButtonText: {
|
||||
color: colors.textInverse,
|
||||
fontSize: 16,
|
||||
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.md,
|
||||
@@ -466,4 +534,101 @@ const styles = StyleSheet.create({
|
||||
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.separator,
|
||||
},
|
||||
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 || '#E0E0E0',
|
||||
borderRadius: borderRadius.md,
|
||||
padding: spacing.md,
|
||||
fontSize: 16,
|
||||
color: colors.text,
|
||||
},
|
||||
modalFeedback: {
|
||||
padding: spacing.md,
|
||||
borderRadius: borderRadius.md,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
modalFeedbackOk: {
|
||||
backgroundColor: 'rgba(0, 69, 13, 0.1)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(0, 69, 13, 0.2)',
|
||||
},
|
||||
modalFeedbackErr: {
|
||||
backgroundColor: 'rgba(186, 26, 26, 0.1)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(186, 26, 26, 0.2)',
|
||||
},
|
||||
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.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border || '#E0E0E0',
|
||||
},
|
||||
modalCancelText: {
|
||||
fontSize: 16,
|
||||
color: colors.text,
|
||||
},
|
||||
modalSaveBtn: {
|
||||
backgroundColor: colors.primary,
|
||||
paddingVertical: spacing.md,
|
||||
paddingHorizontal: spacing.xl,
|
||||
borderRadius: borderRadius.md,
|
||||
minWidth: 100,
|
||||
alignItems: 'center',
|
||||
},
|
||||
modalSaveBtnDisabled: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
modalSaveText: {
|
||||
color: colors.textInverse,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user