feat: read-only name fields with Configuración modal for profile editing

This commit is contained in:
Antoni Nuñez Romeu
2026-07-07 01:37:57 +02:00
parent c46a3ee7ea
commit b899d9a297
3 changed files with 269 additions and 58 deletions
+136 -53
View File
@@ -5,17 +5,24 @@ function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
const [firstName, setFirstName] = useState(currentUser?.first_name || '');
const [lastName, setLastName] = useState(currentUser?.last_name || '');
const [avatarUrl, setAvatarUrl] = useState(currentUser?.avatar_url || '');
const [saving, setSaving] = useState(false);
const [feedback, setFeedback] = useState(null);
const [searchHistory, setSearchHistory] = useState([]);
const [uploading, setUploading] = useState(false);
const fileInputRef = useRef(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(null);
useEffect(() => {
setFirstName(currentUser?.first_name || '');
setLastName(currentUser?.last_name || '');
setAvatarUrl(currentUser?.avatar_url || '');
setFeedback(null);
loadSearchHistory();
}, [currentUser?.id]);
@@ -31,19 +38,31 @@ function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
}
}
async function handleSave(e) {
function openConfig() {
setConfigFirstName(currentUser?.first_name || '');
setConfigLastName(currentUser?.last_name || '');
setConfigEmail(currentUser?.email || '');
setConfigCity(currentUser?.city || '');
setConfigAddress(currentUser?.address || '');
setConfigFeedback(null);
setShowConfig(true);
}
async function handleConfigSave(e) {
e?.preventDefault();
setSaving(true);
setFeedback(null);
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) {
@@ -52,11 +71,14 @@ function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
}
const updated = await res.json();
onProfileSaved?.(updated);
setFeedback({ type: 'ok', text: 'Perfil guardado.' });
setFirstName(updated.first_name || '');
setLastName(updated.last_name || '');
setConfigFeedback({ type: 'ok', text: 'Perfil guardado.' });
setTimeout(() => setShowConfig(false), 1200);
} catch (err) {
setFeedback({ type: 'err', text: err.message || 'Error al guardar' });
setConfigFeedback({ type: 'err', text: err.message || 'Error al guardar' });
} finally {
setSaving(false);
setConfigSaving(false);
}
}
@@ -65,7 +87,6 @@ function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
if (!file) return;
setUploading(true);
setFeedback(null);
try {
const reader = new FileReader();
reader.onload = async () => {
@@ -81,16 +102,15 @@ function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
if (res.ok) {
const updated = await res.json();
onProfileSaved?.(updated);
setFeedback({ type: 'ok', text: 'Foto de perfil actualizada.' });
}
} catch (err) {
setFeedback({ type: 'err', text: 'Error al guardar la foto' });
console.error('Error saving avatar:', err);
}
setUploading(false);
};
reader.readAsDataURL(file);
} catch (err) {
setFeedback({ type: 'err', text: 'Error al procesar la imagen' });
console.error('Error processing image:', err);
setUploading(false);
}
}
@@ -142,46 +162,32 @@ function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
{uploading && <p className="profile-section-sub">Subiendo foto...</p>}
</div>
{/* Editable Name Section */}
<form onSubmit={handleSave} className="profile-form">
<div className="profile-info-cards">
<div className="profile-info-card">
<label className="info-card-label">Nombre</label>
<input
type="text"
className="info-card-input"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
placeholder="Tu nombre"
disabled={saving}
/>
</div>
<div className="profile-info-card">
<label className="info-card-label">Apellidos</label>
<input
type="text"
className="info-card-input"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
placeholder="Tus apellidos"
disabled={saving}
/>
</div>
{/* Read-only Name Section */}
<div className="profile-info-cards">
<div className="profile-info-card">
<p className="info-card-label">Nombre</p>
<p className="info-card-value">{firstName || '—'}</p>
</div>
{feedback && (
<p className={`profile-feedback profile-feedback--${feedback.type}`}>{feedback.text}</p>
)}
<div className="profile-actions">
<button type="submit" className="profile-btn-primary" disabled={saving}>
{saving ? 'Guardando...' : 'Guardar Cambios'}
</button>
<div className="profile-info-card">
<p className="info-card-label">Apellidos</p>
<p className="info-card-value">{lastName || '—'}</p>
</div>
</form>
</div>
{/* Search History Section */}
{/* Menu Section */}
<div className="profile-menu">
<button className="profile-menu-item" onClick={openConfig}>
<div className="menu-item-icon menu-item-icon--secondary">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z" />
</svg>
</div>
<span className="menu-item-label">Configuración</span>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="menu-item-chevron">
<polyline points="9 18 15 12 9 6" />
</svg>
</button>
<div className="profile-menu-item">
<div className="menu-item-icon menu-item-icon--primary">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
@@ -237,8 +243,85 @@ function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
</div>
<span>Cerrar Sesión</span>
</button>
{/* Config Modal */}
{showConfig && (
<div className="profile-modal-backdrop" onClick={() => setShowConfig(false)}>
<div className="profile-modal" onClick={(e) => e.stopPropagation()}>
<div className="profile-modal-header">
<h3>Configuración</h3>
<button className="profile-modal-close" onClick={() => setShowConfig(false)}>×</button>
</div>
<form onSubmit={handleConfigSave} className="profile-modal-body">
<div className="profile-modal-field">
<label>Nombre</label>
<input
type="text"
value={configFirstName}
onChange={(e) => setConfigFirstName(e.target.value)}
placeholder="Tu nombre"
disabled={configSaving}
/>
</div>
<div className="profile-modal-field">
<label>Apellidos</label>
<input
type="text"
value={configLastName}
onChange={(e) => setConfigLastName(e.target.value)}
placeholder="Tus apellidos"
disabled={configSaving}
/>
</div>
<div className="profile-modal-field">
<label>Correo electrónico</label>
<input
type="email"
value={configEmail}
onChange={(e) => setConfigEmail(e.target.value)}
placeholder="tu@email.com"
disabled={configSaving}
/>
</div>
<div className="profile-modal-field">
<label>Ciudad</label>
<input
type="text"
value={configCity}
onChange={(e) => setConfigCity(e.target.value)}
placeholder="Tu ciudad"
disabled={configSaving}
/>
</div>
<div className="profile-modal-field">
<label>Dirección</label>
<input
type="text"
value={configAddress}
onChange={(e) => setConfigAddress(e.target.value)}
placeholder="Calle Mayor 1, Madrid"
disabled={configSaving}
/>
</div>
{configFeedback && (
<p className={`profile-feedback profile-feedback--${configFeedback.type}`}>{configFeedback.text}</p>
)}
<div className="profile-modal-actions">
<button type="button" className="profile-btn-cancel" onClick={() => setShowConfig(false)} disabled={configSaving}>
Cancelar
</button>
<button type="submit" className="profile-btn-primary" disabled={configSaving}>
{configSaving ? 'Guardando...' : 'Guardar'}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}
export default ProfileView;
export default ProfileView;