Files
FarmaFinder/apps/frontend/src/views/ProfileView.jsx
T

328 lines
14 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;