From c5417ff78703195ee2d6c4b65547747ba50354d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Tue, 7 Jul 2026 01:38:38 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20mobile=20profile=20with=20read-only=20f?= =?UTF-8?q?ields=20and=20Configuraci=C3=B3n=20modal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend-mobile/app/(tabs)/profile.tsx | 365 ++++++++++++++------ 1 file changed, 265 insertions(+), 100 deletions(-) diff --git a/apps/frontend-mobile/app/(tabs)/profile.tsx b/apps/frontend-mobile/app/(tabs)/profile.tsx index 03e5f20..8711d44 100644 --- a/apps/frontend-mobile/app/(tabs)/profile.tsx +++ b/apps/frontend-mobile/app/(tabs)/profile.tsx @@ -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([]); - 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); } } } @@ -121,8 +146,8 @@ export default function ProfileScreen() { '¿Estás seguro que deseas cerrar sesión?', [ { text: 'Cancelar', style: 'cancel' }, - { - text: 'Cerrar Sesión', + { + text: 'Cerrar Sesión', style: 'destructive', onPress: async () => { await logout(); @@ -184,50 +209,26 @@ export default function ProfileScreen() { Toca para cambiar foto - {/* Editable Fields */} - - - Nombre - + {/* Read-only Name Section */} + + + Nombre + {firstName || '—'} - - Apellidos - + + Apellidos + {lastName || '—'} - - {feedback && ( - - {feedback.text} - - )} - - - {saving ? ( - - ) : ( - Guardar Cambios - )} - {/* Menu Section */} + + + Configuración + + + Mis Direcciones @@ -240,7 +241,7 @@ export default function ProfileScreen() { {searchHistory.map((item) => ( {item.address} - handleDeleteSearch(item.id)} style={styles.searchDelete} > @@ -265,6 +266,101 @@ export default function ProfileScreen() { Cerrar Sesión + + {/* Config Modal */} + + + + + Configuración + setShowConfig(false)}> + + + + + + Nombre + + + + Apellidos + + + + Correo electrónico + + + + Ciudad + + + + Dirección + + + + {configFeedback && ( + + {configFeedback.text} + + )} + + + setShowConfig(false)} + disabled={configSaving} + > + Cancelar + + + {configSaving ? ( + + ) : ( + Guardar + )} + + + + + + ); } @@ -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', + }, });