feat: read-only name fields with Configuración modal for profile editing
This commit is contained in:
+15
-5
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
{/* Read-only Name Section */}
|
||||
<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}
|
||||
/>
|
||||
<p className="info-card-label">Nombre</p>
|
||||
<p className="info-card-value">{firstName || '—'}</p>
|
||||
</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}
|
||||
/>
|
||||
<p className="info-card-label">Apellidos</p>
|
||||
<p className="info-card-value">{lastName || '—'}</p>
|
||||
</div>
|
||||
</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>
|
||||
</form>
|
||||
|
||||
{/* 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,6 +243,83 @@ 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user