328 lines
14 KiB
React
328 lines
14 KiB
React
import React, { useEffect, useState, useRef } from 'react';
|
||
import './ProfileView.css';
|
||
|
||
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 [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 || '');
|
||
loadSearchHistory();
|
||
}, [currentUser?.id]);
|
||
|
||
async function loadSearchHistory() {
|
||
try {
|
||
const res = await fetch('/api/search-history', { credentials: 'include' });
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
setSearchHistory(data);
|
||
}
|
||
} catch (err) {
|
||
console.error('Error loading search history:', err);
|
||
}
|
||
}
|
||
|
||
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();
|
||
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: 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 (HTTP ${res.status})`);
|
||
}
|
||
const updated = await res.json();
|
||
onProfileSaved?.(updated);
|
||
setFirstName(updated.first_name || '');
|
||
setLastName(updated.last_name || '');
|
||
setConfigFeedback({ type: 'ok', text: 'Perfil guardado.' });
|
||
setTimeout(() => setShowConfig(false), 1200);
|
||
} catch (err) {
|
||
setConfigFeedback({ type: 'err', text: err.message || 'Error al guardar' });
|
||
} finally {
|
||
setConfigSaving(false);
|
||
}
|
||
}
|
||
|
||
async function handleAvatarUpload(e) {
|
||
const file = e.target.files?.[0];
|
||
if (!file) return;
|
||
|
||
setUploading(true);
|
||
try {
|
||
const reader = new FileReader();
|
||
reader.onload = async () => {
|
||
const base64 = reader.result;
|
||
setAvatarUrl(base64);
|
||
try {
|
||
const res = await fetch('/api/users/me', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ avatar_url: base64 }),
|
||
});
|
||
if (res.ok) {
|
||
const updated = await res.json();
|
||
onProfileSaved?.(updated);
|
||
}
|
||
} catch (err) {
|
||
console.error('Error saving avatar:', err);
|
||
}
|
||
setUploading(false);
|
||
};
|
||
reader.readAsDataURL(file);
|
||
} catch (err) {
|
||
console.error('Error processing image:', err);
|
||
setUploading(false);
|
||
}
|
||
}
|
||
|
||
async function handleDeleteSearch(id) {
|
||
try {
|
||
const res = await fetch(`/api/search-history/${id}`, {
|
||
method: 'DELETE',
|
||
credentials: 'include',
|
||
});
|
||
if (res.ok) {
|
||
setSearchHistory(prev => prev.filter(item => item.id !== id));
|
||
}
|
||
} catch (err) {
|
||
console.error('Error deleting search:', err);
|
||
}
|
||
}
|
||
|
||
const displayName = [firstName, lastName].filter(Boolean).join(' ') || currentUser?.username || 'Usuario';
|
||
|
||
return (
|
||
<div className="profile-view">
|
||
{/* Avatar Section */}
|
||
<div className="profile-avatar-section">
|
||
<div
|
||
className="profile-avatar-circle profile-avatar-editable"
|
||
onClick={() => fileInputRef.current?.click()}
|
||
>
|
||
{avatarUrl ? (
|
||
<img src={avatarUrl} alt="Avatar" className="profile-avatar-image" />
|
||
) : (
|
||
<svg width="80" height="80" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
|
||
</svg>
|
||
)}
|
||
<div className="profile-avatar-overlay">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
<input
|
||
ref={fileInputRef}
|
||
type="file"
|
||
accept="image/*"
|
||
onChange={handleAvatarUpload}
|
||
style={{ display: 'none' }}
|
||
/>
|
||
{uploading && <p className="profile-section-sub">Subiendo foto...</p>}
|
||
</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>
|
||
<div className="profile-info-card">
|
||
<p className="info-card-label">Apellidos</p>
|
||
<p className="info-card-value">{lastName || '—'}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 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">
|
||
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" />
|
||
</svg>
|
||
</div>
|
||
<span className="menu-item-label">Mis Direcciones</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>
|
||
</div>
|
||
|
||
{searchHistory.length > 0 && (
|
||
<div className="profile-search-history">
|
||
<p className="profile-section-sub">Tus búsquedas recientes:</p>
|
||
{searchHistory.map((item) => (
|
||
<div key={item.id} className="profile-search-item">
|
||
<span className="profile-search-address">{item.address}</span>
|
||
<button
|
||
className="profile-search-delete"
|
||
onClick={() => handleDeleteSearch(item.id)}
|
||
title="Eliminar"
|
||
>
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{currentUser?.is_admin && (
|
||
<button className="profile-menu-item" onClick={onAdminClick}>
|
||
<div className="menu-item-icon menu-item-icon--primary">
|
||
<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">Panel de Administració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>
|
||
|
||
<button className="profile-logout-btn" onClick={onLogout}>
|
||
<div className="menu-item-icon menu-item-icon--error-outline">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z" />
|
||
</svg>
|
||
</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;
|