From b899d9a29700bc753de40fa0a6d89326d19a4fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Tue, 7 Jul 2026 01:37:57 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20read-only=20name=20fields=20with=20Conf?= =?UTF-8?q?iguraci=C3=B3n=20modal=20for=20profile=20editing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/server.js | 20 ++- apps/frontend/src/views/ProfileView.css | 118 +++++++++++++++ apps/frontend/src/views/ProfileView.jsx | 189 +++++++++++++++++------- 3 files changed, 269 insertions(+), 58 deletions(-) diff --git a/apps/backend/server.js b/apps/backend/server.js index d6d10c0..52d9f52 100644 --- a/apps/backend/server.js +++ b/apps/backend/server.js @@ -296,11 +296,15 @@ if (!pgPool) { ALTER TABLE users ADD COLUMN IF NOT EXISTS first_name TEXT; ALTER TABLE users ADD COLUMN IF NOT EXISTS last_name TEXT; ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_url TEXT; + ALTER TABLE users ADD COLUMN IF NOT EXISTS email TEXT; + ALTER TABLE users ADD COLUMN IF NOT EXISTS city TEXT; `); } else { try { await dbRun('ALTER TABLE users ADD COLUMN first_name TEXT'); } catch {} try { await dbRun('ALTER TABLE users ADD COLUMN last_name TEXT'); } catch {} try { await dbRun('ALTER TABLE users ADD COLUMN avatar_url TEXT'); } catch {} + try { await dbRun('ALTER TABLE users ADD COLUMN email TEXT'); } catch {} + try { await dbRun('ALTER TABLE users ADD COLUMN city TEXT'); } catch {} } // Add user_id to push tables (SQLite — no cross-DB FK) @@ -816,7 +820,7 @@ app.get('/api/auth/check', async (req, res) => { app.get('/api/users/me', requireAuth, async (req, res) => { try { const user = await userDbGet( - 'SELECT id, username, first_name, last_name, avatar_url, is_admin, address, latitude, longitude FROM users WHERE id = ?', + 'SELECT id, username, first_name, last_name, avatar_url, email, city, is_admin, address, latitude, longitude FROM users WHERE id = ?', [req.session.userId] ); if (!user) return res.status(404).json({ error: 'Not found' }); @@ -826,6 +830,8 @@ app.get('/api/users/me', requireAuth, async (req, res) => { first_name: user.first_name || null, last_name: user.last_name || null, avatar_url: user.avatar_url || null, + email: user.email || null, + city: user.city || null, is_admin: Boolean(user.is_admin), address: user.address || null, latitude: user.latitude, @@ -840,11 +846,13 @@ app.get('/api/users/me', requireAuth, async (req, res) => { // Update the current user's address + coordinates app.put('/api/users/me', requireAuth, async (req, res) => { try { - const { first_name, last_name, avatar_url, address, latitude, longitude } = req.body || {}; + const { first_name, last_name, avatar_url, email, city, address, latitude, longitude } = req.body || {}; const fName = first_name == null ? null : String(first_name).trim().slice(0, 100); const lName = last_name == null ? null : String(last_name).trim().slice(0, 100); const avatar = avatar_url == null ? null : String(avatar_url).slice(0, 500); + const userEmail = email == null ? null : String(email).trim().slice(0, 200); + const userCity = city == null ? null : String(city).trim().slice(0, 200); const addr = address == null || String(address).trim() === '' @@ -867,12 +875,12 @@ app.put('/api/users/me', requireAuth, async (req, res) => { } await userDbRun( - 'UPDATE users SET first_name = ?, last_name = ?, avatar_url = ?, address = ?, latitude = ?, longitude = ? WHERE id = ?', - [fName, lName, avatar, addr, lat, lon, req.session.userId] + 'UPDATE users SET first_name = ?, last_name = ?, avatar_url = ?, email = ?, city = ?, address = ?, latitude = ?, longitude = ? WHERE id = ?', + [fName, lName, avatar, userEmail, userCity, addr, lat, lon, req.session.userId] ); const user = await userDbGet( - 'SELECT id, username, first_name, last_name, avatar_url, is_admin, address, latitude, longitude FROM users WHERE id = ?', + 'SELECT id, username, first_name, last_name, avatar_url, email, city, is_admin, address, latitude, longitude FROM users WHERE id = ?', [req.session.userId] ); res.json({ @@ -881,6 +889,8 @@ app.put('/api/users/me', requireAuth, async (req, res) => { first_name: user.first_name || null, last_name: user.last_name || null, avatar_url: user.avatar_url || null, + email: user.email || null, + city: user.city || null, is_admin: Boolean(user.is_admin), address: user.address || null, latitude: user.latitude, diff --git a/apps/frontend/src/views/ProfileView.css b/apps/frontend/src/views/ProfileView.css index b0e698a..364e2ce 100644 --- a/apps/frontend/src/views/ProfileView.css +++ b/apps/frontend/src/views/ProfileView.css @@ -442,3 +442,121 @@ background: rgba(186, 26, 26, 0.1); color: var(--error); } + +/* Config Modal */ +.profile-modal-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 1rem; +} + +.profile-modal { + background: var(--surface-container-lowest); + border-radius: var(--radius-md); + width: 100%; + max-width: 28rem; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); +} + +.profile-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.25rem 1.5rem; + border-bottom: 1px solid var(--outline-variant); +} + +.profile-modal-header h3 { + margin: 0; + font-size: 1.25rem; + font-weight: 700; + color: var(--on-surface); +} + +.profile-modal-close { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: var(--on-surface-variant); + padding: 0.25rem; + line-height: 1; +} + +.profile-modal-body { + padding: 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.profile-modal-field { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.profile-modal-field label { + font-size: 0.8rem; + font-weight: 600; + color: var(--on-surface-variant); + letter-spacing: 0.03em; +} + +.profile-modal-field input { + padding: 0.7rem 0.85rem; + border: 2px solid var(--outline-variant); + border-radius: var(--radius); + background: var(--surface); + color: var(--on-surface); + font-size: 0.95rem; + font-family: inherit; + outline: none; + transition: border-color 0.15s; +} + +.profile-modal-field input:focus { + border-color: var(--primary); +} + +.profile-modal-field input:disabled { + opacity: 0.6; +} + +.profile-modal-actions { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 0.5rem; +} + +.profile-btn-cancel { + background: var(--surface-container-low); + border: 1px solid var(--outline-variant); + color: var(--on-surface); + padding: 0.65rem 1.25rem; + border-radius: var(--radius-full); + cursor: pointer; + font-size: 0.9rem; + font-weight: 600; + font-family: inherit; + transition: background 0.15s; +} + +.profile-btn-cancel:hover:not(:disabled) { + background: var(--surface-container); +} + +.profile-btn-cancel:disabled { + opacity: 0.6; +} diff --git a/apps/frontend/src/views/ProfileView.jsx b/apps/frontend/src/views/ProfileView.jsx index af0fdb7..e3e9f72 100644 --- a/apps/frontend/src/views/ProfileView.jsx +++ b/apps/frontend/src/views/ProfileView.jsx @@ -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 &&

Subiendo foto...

} - {/* Editable Name Section */} -
-
-
- - setFirstName(e.target.value)} - placeholder="Tu nombre" - disabled={saving} - /> -
-
- - setLastName(e.target.value)} - placeholder="Tus apellidos" - disabled={saving} - /> -
+ {/* Read-only Name Section */} +
+
+

Nombre

+

{firstName || '—'}

- - {feedback && ( -

{feedback.text}

- )} - -
- +
+

Apellidos

+

{lastName || '—'}

- +
- {/* Search History Section */} + {/* Menu Section */}
+ +
@@ -237,8 +243,85 @@ function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
Cerrar Sesión + + {/* Config Modal */} + {showConfig && ( +
setShowConfig(false)}> +
e.stopPropagation()}> +
+

Configuración

+ +
+
+
+ + setConfigFirstName(e.target.value)} + placeholder="Tu nombre" + disabled={configSaving} + /> +
+
+ + setConfigLastName(e.target.value)} + placeholder="Tus apellidos" + disabled={configSaving} + /> +
+
+ + setConfigEmail(e.target.value)} + placeholder="tu@email.com" + disabled={configSaving} + /> +
+
+ + setConfigCity(e.target.value)} + placeholder="Tu ciudad" + disabled={configSaving} + /> +
+
+ + setConfigAddress(e.target.value)} + placeholder="Calle Mayor 1, Madrid" + disabled={configSaving} + /> +
+ + {configFeedback && ( +

{configFeedback.text}

+ )} + +
+ + +
+
+
+
+ )}
); } -export default ProfileView; \ No newline at end of file +export default ProfileView;