improve profile

This commit is contained in:
Nicola Malizia
2025-10-10 20:17:57 +02:00
parent 7c4feddfa5
commit bc21450776
7 changed files with 576 additions and 97 deletions

View File

@@ -35,6 +35,12 @@ function CelebrityProfile() {
const handleFieldSave = async (fieldName, newValue) => { const handleFieldSave = async (fieldName, newValue) => {
// Converte stringa vuota a null per il backend // Converte stringa vuota a null per il backend
const valueToSend = newValue === '' ? null : newValue; const valueToSend = newValue === '' ? null : newValue;
// Converti la stringa 'true'/'false' in un booleano per il campo specifico
if (fieldName === 'boobs_are_natural') {
valueToSend = newValue === 'true';
}
const payload = { [fieldName]: valueToSend }; const payload = { [fieldName]: valueToSend };
try { try {

249
gemini-schema.json Normal file
View File

@@ -0,0 +1,249 @@
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Il nome principale della celebrità.",
"minLength": 1
},
"gender": {
"type": "string",
"enum": [
"male",
"female",
"other"
],
"description": "Il genere della celebrità."
},
"birth_date": {
"type": "string",
"format": "date",
"description": "La data di nascita in formato ISO 8601 (YYYY-MM-DD)."
},
"birth_place": {
"type": "string"
},
"nationality": {
"type": "string"
},
"ethnicity": {
"type": "string"
},
"sexuality": {
"type": "string"
},
"physical_details": {
"type": "object",
"properties": {
"hair_color": {
"type": "string"
},
"eye_color": {
"type": "string"
},
"height_cm": {
"type": "integer",
"minimum": 1
},
"weight_kg": {
"type": "integer",
"minimum": 1
},
"body_type": {
"type": "string"
},
"measurements_cm": {
"type": "object",
"properties": {
"bust": {
"type": "integer",
"minimum": 1
},
"waist": {
"type": "integer",
"minimum": 1
},
"hips": {
"type": "integer",
"minimum": 1
},
"chest_circumference": {
"type": "integer",
"minimum": 1
}
},
"propertyOrdering": [
"bust",
"waist",
"hips",
"chest_circumference"
],
"description": "Misure del corpo in centimetri."
},
"bra_size": {
"type": "object",
"properties": {
"band": {
"type": "integer",
"minimum": 1
},
"cup": {
"type": "string"
},
"system": {
"type": "string",
"enum": [
"US",
"UK",
"EU",
"FR",
"AU",
"IT",
"JP"
]
}
},
"propertyOrdering": [
"band",
"cup",
"system"
],
"required": [
"band",
"cup",
"system"
],
"description": "La taglia attuale del reggiseno."
},
"boobs_are_natural": {
"type": "boolean"
},
"shoe_size": {
"type": "object",
"properties": {
"size": {
"type": "number"
},
"system": {
"type": "string",
"enum": [
"EU",
"US",
"UK"
]
}
},
"propertyOrdering": [
"size",
"system"
],
"required": [
"size",
"system"
]
}
},
"propertyOrdering": [
"hair_color",
"eye_color",
"height_cm",
"weight_kg",
"body_type",
"measurements_cm",
"bra_size",
"boobs_are_natural",
"shoe_size"
],
"description": "Dettagli fisici e misure della celebrità."
},
"biography": {
"type": "string"
},
"official_website": {
"type": "string",
"format": "uri"
},
"aliases": {
"type": "array",
"items": {
"type": "string"
},
"description": "Elenco di nomi alternativi o alias."
},
"professions": {
"type": "array",
"items": {
"type": "string"
},
"description": "Elenco delle professioni della celebrità."
},
"activity_periods": {
"type": "array",
"items": {
"type": "object",
"properties": {
"start_year": {
"type": "integer",
"minimum": 1900,
"maximum": 2100
},
"end_year": {
"type": "integer",
"minimum": 1900,
"maximum": 2100
},
"notes": {
"type": "string"
}
},
"propertyOrdering": [
"start_year",
"end_year",
"notes"
],
"required": [
"start_year"
]
},
"description": "Periodi di attività noti della carriera."
},
"tattoos": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"body_location": {
"type": "string"
}
},
"propertyOrdering": [
"description",
"body_location"
],
"required": [
"description"
]
},
"description": "Elenco dei tatuaggi conosciuti."
}
},
"propertyOrdering": [
"name",
"gender",
"birth_date",
"birth_place",
"nationality",
"ethnicity",
"sexuality",
"physical_details",
"biography",
"official_website",
"aliases",
"professions",
"activity_periods",
"tattoos"
]
}

View File

@@ -8,9 +8,11 @@ const initialFormState = {
birth_date: '', birth_date: '',
height_cm: '', height_cm: '',
weight_kg: '', weight_kg: '',
body_type: '',
bust_cm: '', bust_cm: '',
waist_cm: '', waist_cm: '',
hips_cm: '', hips_cm: '',
chest_circumference_cm: '',
bra_band_size: '', bra_band_size: '',
bra_cup_size: '', bra_cup_size: '',
bra_size_system: 'US', bra_size_system: 'US',
@@ -22,6 +24,9 @@ const initialFormState = {
hair_color: '', hair_color: '',
eye_color: '', eye_color: '',
biography: '', biography: '',
shoe_size: '',
shoe_size_system: 'EU',
official_website: '',
}; };
function CelebrityCreate() { function CelebrityCreate() {
@@ -84,6 +89,10 @@ function CelebrityCreate() {
<label>Luogo di Nascita<input type="text" name="birth_place" value={formData.birth_place} onChange={handleChange} /></label> <label>Luogo di Nascita<input type="text" name="birth_place" value={formData.birth_place} onChange={handleChange} /></label>
<label>Nazionalità<input type="text" name="nationality" value={formData.nationality} onChange={handleChange} /></label> <label>Nazionalità<input type="text" name="nationality" value={formData.nationality} onChange={handleChange} /></label>
</div> </div>
<label>
Sito Ufficiale
<input type="url" name="official_website" value={formData.official_website} onChange={handleChange} placeholder="https://..."/>
</label>
</fieldset> </fieldset>
<fieldset> <fieldset>
@@ -91,11 +100,23 @@ function CelebrityCreate() {
<div className="grid"> <div className="grid">
<label>Altezza (cm)<input type="number" name="height_cm" value={formData.height_cm} onChange={handleChange} /></label> <label>Altezza (cm)<input type="number" name="height_cm" value={formData.height_cm} onChange={handleChange} /></label>
<label>Peso (kg)<input type="number" name="weight_kg" value={formData.weight_kg} onChange={handleChange} /></label> <label>Peso (kg)<input type="number" name="weight_kg" value={formData.weight_kg} onChange={handleChange} /></label>
<label>Seno (cm)<input type="number" name="bust_cm" value={formData.bust_cm} onChange={handleChange} /></label> <label>Corporatura<input type="text" name="body_type" value={formData.body_type} onChange={handleChange} /></label>
</div> </div>
<div className="grid"> <div className="grid">
<label>Seno (cm)<input type="number" name="bust_cm" value={formData.bust_cm} onChange={handleChange} /></label>
<label>Vita (cm)<input type="number" name="waist_cm" value={formData.waist_cm} onChange={handleChange} /></label> <label>Vita (cm)<input type="number" name="waist_cm" value={formData.waist_cm} onChange={handleChange} /></label>
<label>Fianchi (cm)<input type="number" name="hips_cm" value={formData.hips_cm} onChange={handleChange} /></label> <label>Fianchi (cm)<input type="number" name="hips_cm" value={formData.hips_cm} onChange={handleChange} /></label>
{formData.gender === 'male' && <label>Torace (cm)<input type="number" name="chest_circumference_cm" value={formData.chest_circumference_cm} onChange={handleChange} /></label>}
</div>
<div className="grid">
<label>Misura Scarpe<input type="number" step="0.5" name="shoe_size" value={formData.shoe_size} onChange={handleChange} /></label>
<label>Sistema
<select name="shoe_size_system" value={formData.shoe_size_system} onChange={handleChange}>
<option value="EU">EU</option> <option value="US">US</option> <option value="UK">UK</option>
</select>
</label>
</div>
<div className="grid">
<label>Colore Capelli<input type="text" name="hair_color" value={formData.hair_color} onChange={handleChange} /></label> <label>Colore Capelli<input type="text" name="hair_color" value={formData.hair_color} onChange={handleChange} /></label>
<label>Colore Occhi<input type="text" name="eye_color" value={formData.eye_color} onChange={handleChange} /></label> <label>Colore Occhi<input type="text" name="eye_color" value={formData.eye_color} onChange={handleChange} /></label>
</div> </div>

View File

@@ -0,0 +1,81 @@
/* packages/frontend/src/components/CelebrityProfile.css */
.profile-container {
display: flex;
gap: 2rem;
align-items: flex-start;
margin-top: 2rem;
}
.profile-sidebar {
flex: 0 0 300px; /* Larghezza fissa per la sidebar */
position: sticky;
top: 2rem;
}
.profile-sidebar figure {
margin: 0;
text-align: center;
}
.profile-sidebar img {
border-radius: var(--border-radius);
margin-bottom: 1rem;
width: 100%;
height: auto;
object-fit: cover;
}
.profile-sidebar .editable-field-container {
text-align: center;
}
.profile-sidebar .editable-field-container label {
display: none; /* Nasconde l'etichetta "Nome" sotto la foto */
}
.profile-sidebar .display-value {
font-size: 1.5rem;
font-weight: bold;
}
.profile-actions {
margin-top: 1.5rem;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
.profile-content {
flex: 1 1 auto; /* Occupa lo spazio rimanente */
min-width: 0;
}
.profile-content details {
border: 1px solid var(--pico-muted-border-color);
border-radius: var(--border-radius);
margin-bottom: 1rem;
padding: 0.5rem 1rem;
}
.profile-content summary {
cursor: pointer;
font-weight: bold;
padding: 0.5rem 0;
}
.profile-content h3 {
margin: 0;
display: inline;
font-size: 1.2rem;
}
.profile-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-top: 1rem;
}
.error-message {
color: var(--pico-color-red-500);
}

View File

@@ -4,6 +4,7 @@ import React, { useState, useEffect } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom'; import { useParams, useNavigate, Link } from 'react-router-dom';
import { getCelebrityById, updateCelebrity, deleteCelebrity } from '../services/api'; import { getCelebrityById, updateCelebrity, deleteCelebrity } from '../services/api';
import EditableField from './EditableField'; import EditableField from './EditableField';
import './CelebrityProfile.css'; // Importa il nuovo CSS per il layout del profilo
function CelebrityProfile() { function CelebrityProfile() {
const { id } = useParams(); const { id } = useParams();
@@ -25,100 +26,116 @@ function CelebrityProfile() {
.catch((err) => setError(err.message)) .catch((err) => setError(err.message))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
} else { } else {
navigate('/'); navigate('/');
} }
}, [id, navigate]); }, [id, navigate]);
const handleFieldSave = async (fieldName, newValue) => { const handleFieldSave = async (fieldName, newValue) => {
const valueToSend = newValue === '' ? null : newValue; let valueToSend = newValue === '' ? null : newValue;
if (fieldName === 'boobs_are_natural') {
valueToSend = newValue === 'true';
}
const payload = { [fieldName]: valueToSend }; const payload = { [fieldName]: valueToSend };
try { try {
await updateCelebrity(id, payload); const updatedCelebrity = await updateCelebrity(id, payload);
// Aggiorna lo stato locale per un feedback immediato
setCelebrity((prev) => ({ ...prev, [fieldName]: newValue })); setCelebrity((prev) => ({ ...prev, [fieldName]: newValue }));
console.log('Salvataggio riuscito:', updatedCelebrity);
} catch (err) { } catch (err) {
setError(`Errore durante il salvataggio: ${err.message}`); console.error(`Errore durante il salvataggio del campo ${fieldName}:`, err);
// Rilancia l'errore in modo che il componente figlio possa gestirlo
throw err;
} }
}; };
const handleDelete = async () => { const handleDelete = async () => {
if (window.confirm(`Sei sicuro di voler eliminare ${celebrity.name}? L'azione è irreversibile.`)) { if (window.confirm(`Sei sicuro di voler eliminare ${celebrity.name}? L'azione è irreversibile.`)) {
try { try {
await deleteCelebrity(id); await deleteCelebrity(id);
navigate('/'); alert(`${celebrity.name} è stato eliminato con successo.`);
} catch (err) { navigate('/');
setError(`Errore durante l'eliminazione: ${err.message}`); } catch (err) {
} setError(`Errore durante l'eliminazione: ${err.message}`);
}
} }
}; };
if (loading) return <article aria-busy="true">Caricamento profilo...</article>; if (loading) return <article aria-busy="true">Caricamento profilo...</article>;
if (error) return <article><p className="error-message">Errore: {error}</p></article>; if (error && !celebrity) return <article><p className="error-message">Errore: {error}</p></article>;
if (!celebrity) return <p>Nessuna celebrità trovata.</p>; if (!celebrity) return <p>Nessuna celebrità trovata.</p>;
const genderOptions = [ { value: 'female', label: 'Female' }, { value: 'male', label: 'Male' }, { value: 'other', label: 'Other' } ]; const genderOptions = [{ value: 'female', label: 'Female' }, { value: 'male', label: 'Male' }, { value: 'other', label: 'Other' }];
const braSystemOptions = [ { value: 'US', label: 'US' }, { value: 'UK', label: 'UK' }, { value: 'EU', label: 'EU' }, { value: 'FR', label: 'FR' }, { value: 'AU', label: 'AU' }, { value: 'IT', label: 'IT' }, { value: 'JP', label: 'JP' }]; const braSystemOptions = [{ value: 'US', label: 'US' }, { value: 'UK', label: 'UK' }, { value: 'EU', label: 'EU' }, { value: 'FR', label: 'FR' }, { value: 'AU', label: 'AU' }, { value: 'IT', label: 'IT' }, { value: 'JP', label: 'JP' }];
const booleanOptions = [ { value: 'true', label: 'Sì' }, { value: 'false', label: 'No' }]; const shoeSystemOptions = [{ value: 'EU', label: 'EU' }, { value: 'US', label: 'US' }, { value: 'UK', label: 'UK' }];
const booleanOptions = [{ value: 'true', label: 'Sì' }, { value: 'false', label: 'No' }];
return ( return (
<div className="grid"> <div className="profile-container">
<aside> <aside className="profile-sidebar">
<figure> <figure>
<img src={`https://i.pravatar.cc/400?u=${id}`} alt={celebrity.name} style={{ width: '100%' }} /> <img src={`https://i.pravatar.cc/400?u=${id}`} alt={celebrity.name} />
<figcaption>{celebrity.name}</figcaption> <figcaption>
<EditableField label="Nome" name="name" value={celebrity.name} onSave={handleFieldSave} />
</figcaption>
</figure> </figure>
<div className="profile-actions">
<Link to="/" role="button" className="secondary outline">Torna alla Lista</Link>
<button onClick={handleDelete} className="secondary">Elimina Profilo</button>
</div>
</aside> </aside>
<section> <main className="profile-content">
<article> {error && <p className="error-message">Errore: {error}</p>}
<header>
<h2>{celebrity.name}</h2> <details open>
<EditableField label="Nome completo / Alias" name="name" value={celebrity.name} onSave={handleFieldSave} /> <summary><h3>Dati Personali</h3></summary>
</header> <div className="profile-grid">
<h3>Dati Personali</h3>
<div className="grid">
<EditableField label="Data di nascita" name="birth_date" value={celebrity.birth_date} type="date" onSave={handleFieldSave} /> <EditableField label="Data di nascita" name="birth_date" value={celebrity.birth_date} type="date" onSave={handleFieldSave} />
<EditableField label="Luogo di nascita" name="birth_place" value={celebrity.birth_place} onSave={handleFieldSave} /> <EditableField label="Luogo di nascita" name="birth_place" value={celebrity.birth_place} onSave={handleFieldSave} />
<EditableField label="Nazionalità" name="nationality" value={celebrity.nationality} onSave={handleFieldSave} /> <EditableField label="Nazionalità" name="nationality" value={celebrity.nationality} onSave={handleFieldSave} />
</div>
<div className="grid">
<EditableField label="Genere" name="gender" value={celebrity.gender} type="select" options={genderOptions} onSave={handleFieldSave} />
<EditableField label="Etnia" name="ethnicity" value={celebrity.ethnicity} onSave={handleFieldSave} /> <EditableField label="Etnia" name="ethnicity" value={celebrity.ethnicity} onSave={handleFieldSave} />
<EditableField label="Genere" name="gender" value={celebrity.gender} type="select" options={genderOptions} onSave={handleFieldSave} />
<EditableField label="Sessualità" name="sexuality" value={celebrity.sexuality} onSave={handleFieldSave} /> <EditableField label="Sessualità" name="sexuality" value={celebrity.sexuality} onSave={handleFieldSave} />
</div> </div>
<hr/> <EditableField label="Sito Ufficiale" name="official_website" value={celebrity.official_website} type="url" onSave={handleFieldSave} />
<h3>Aspetto Fisico</h3> </details>
<div className="grid">
<details>
<summary><h3>Aspetto Fisico e Misure</h3></summary>
<div className="profile-grid">
<EditableField label="Altezza (cm)" name="height_cm" value={celebrity.height_cm} type="number" onSave={handleFieldSave} /> <EditableField label="Altezza (cm)" name="height_cm" value={celebrity.height_cm} type="number" onSave={handleFieldSave} />
<EditableField label="Peso (kg)" name="weight_kg" value={celebrity.weight_kg} type="number" onSave={handleFieldSave} /> <EditableField label="Peso (kg)" name="weight_kg" value={celebrity.weight_kg} type="number" onSave={handleFieldSave} />
<EditableField label="Corporatura" name="body_type" value={celebrity.body_type} onSave={handleFieldSave} />
<EditableField label="Colore Capelli" name="hair_color" value={celebrity.hair_color} onSave={handleFieldSave} /> <EditableField label="Colore Capelli" name="hair_color" value={celebrity.hair_color} onSave={handleFieldSave} />
<EditableField label="Colore Occhi" name="eye_color" value={celebrity.eye_color} onSave={handleFieldSave} /> <EditableField label="Colore Occhi" name="eye_color" value={celebrity.eye_color} onSave={handleFieldSave} />
</div>
<div className="grid">
<EditableField label="Seno (cm)" name="bust_cm" value={celebrity.bust_cm} type="number" onSave={handleFieldSave} /> <EditableField label="Seno (cm)" name="bust_cm" value={celebrity.bust_cm} type="number" onSave={handleFieldSave} />
<EditableField label="Vita (cm)" name="waist_cm" value={celebrity.waist_cm} type="number" onSave={handleFieldSave} /> <EditableField label="Vita (cm)" name="waist_cm" value={celebrity.waist_cm} type="number" onSave={handleFieldSave} />
<EditableField label="Fianchi (cm)" name="hips_cm" value={celebrity.hips_cm} type="number" onSave={handleFieldSave} /> <EditableField label="Fianchi (cm)" name="hips_cm" value={celebrity.hips_cm} type="number" onSave={handleFieldSave} />
{celebrity.gender === 'male' && <EditableField label="Torace (cm)" name="chest_circumference_cm" value={celebrity.chest_circumference_cm} type="number" onSave={handleFieldSave} />}
<EditableField label="Misura Scarpe" name="shoe_size" value={celebrity.shoe_size} type="number" onSave={handleFieldSave} />
<EditableField label="Sistema Scarpe" name="shoe_size_system" value={celebrity.shoe_size_system} type="select" options={shoeSystemOptions} onSave={handleFieldSave} />
</div> </div>
<hr/> </details>
<h3>Reggiseno</h3>
<div className="grid"> {celebrity.gender !== 'male' && (
<EditableField label="Misura Fascia" name="bra_band_size" value={celebrity.bra_band_size} type="number" onSave={handleFieldSave} /> <details>
<EditableField label="Coppa" name="bra_cup_size" value={celebrity.bra_cup_size} onSave={handleFieldSave} /> <summary><h3>Reggiseno</h3></summary>
<EditableField label="Sistema" name="bra_size_system" value={celebrity.bra_size_system} type="select" options={braSystemOptions} onSave={handleFieldSave} /> <div className="profile-grid">
<EditableField label="Seno Naturale?" name="boobs_are_natural" value={String(celebrity.boobs_are_natural)} type="select" options={booleanOptions} onSave={handleFieldSave} /> <EditableField label="Misura Fascia" name="bra_band_size" value={celebrity.bra_band_size} type="number" onSave={handleFieldSave} />
</div> <EditableField label="Coppa" name="bra_cup_size" value={celebrity.bra_cup_size} onSave={handleFieldSave} />
<hr/> <EditableField label="Sistema" name="bra_size_system" value={celebrity.bra_size_system} type="select" options={braSystemOptions} onSave={handleFieldSave} />
<h3>Biografia</h3> <EditableField label="Seno Naturale?" name="boobs_are_natural" value={String(celebrity.boobs_are_natural)} type="select" options={booleanOptions} onSave={handleFieldSave} />
<EditableField label="Note biografiche" name="biography" value={celebrity.biography} type="textarea" onSave={handleFieldSave} />
<footer>
<div className="grid">
<Link to="/" role="button" className="secondary outline">Torna alla Lista</Link>
<button onClick={handleDelete} className="secondary">Elimina Profilo</button>
</div> </div>
</footer> </details>
</article> )}
</section>
<details>
<summary><h3>Biografia</h3></summary>
<EditableField label="Note biografiche" name="biography" value={celebrity.biography} type="textarea" onSave={handleFieldSave} />
</details>
</main>
</div> </div>
); );
} }

View File

@@ -0,0 +1,86 @@
/* packages/frontend/src/components/EditableField.css */
.editable-field-container {
display: flex;
flex-direction: column;
gap: 0.25rem;
position: relative;
}
.editable-field-container label {
font-size: 0.85rem;
font-weight: 500;
color: var(--pico-secondary);
}
/* Modalità visualizzazione */
.display-mode {
display: flex;
justify-content: space-between;
align-items: center;
min-height: 40px; /* Stessa altezza dell'input */
padding: 0.375rem 0.75rem;
border: 1px solid transparent; /* Per mantenere l'allineamento */
border-radius: var(--border-radius);
transition: background-color 0.2s ease-in-out;
}
.display-mode:hover {
background-color: var(--pico-muted-background-color);
}
.display-mode .display-value {
flex-grow: 1;
word-break: break-word;
}
.display-mode em {
color: var(--pico-secondary);
font-style: normal;
}
.display-mode .button-icon {
visibility: hidden; /* Nascosto di default */
opacity: 0;
transition: opacity 0.2s ease-in-out;
margin-left: 0.5rem;
}
.display-mode:hover .button-icon {
visibility: visible; /* Visibile solo in hover */
opacity: 1;
}
/* Modalità modifica */
.edit-mode {
display: flex;
flex-direction: column;
}
.edit-mode input,
.edit-mode select,
.edit-mode textarea {
width: 100%;
}
.edit-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
justify-content: flex-start;
}
/* Pulsanti piccoli per le azioni */
.button-icon {
padding: 0.3rem 0.5rem;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
.error-text {
color: var(--pico-color-red-500);
font-size: 0.8rem;
margin-top: 0.25rem;
}

View File

@@ -1,40 +1,49 @@
// packages/frontend/src/components/EditableField.jsx // packages/frontend/src/components/EditableField.jsx
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import './EditableField.css'; // Importiamo il nuovo CSS
function EditableField({ // Piccole icone SVG per i pulsanti
label, const EditIcon = () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path></svg>;
name, const SaveIcon = () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>;
value, const CancelIcon = () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>;
onSave,
type = 'text',
options = [], // Per i campi <select> function EditableField({ label, name, value, onSave, type = 'text', options = [] }) {
}) {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
// Usiamo un valore interno per non scatenare re-render del genitore ad ogni tasto premuto
const [currentValue, setCurrentValue] = useState(value); const [currentValue, setCurrentValue] = useState(value);
const [status, setStatus] = useState('idle'); // 'idle', 'saving', 'error'
const [error, setError] = useState('');
// Aggiorna il valore interno se il prop 'value' cambia dall'esterno
useEffect(() => { useEffect(() => {
setCurrentValue(value); setCurrentValue(value);
}, [value]); }, [value]);
const handleSave = () => { const handleSave = async () => {
// Salva solo se il valore è cambiato if (currentValue === value) {
if (currentValue !== value) { setIsEditing(false);
onSave(name, currentValue); return;
}
setStatus('saving');
setError('');
try {
// La funzione onSave ora deve essere una Promise
await onSave(name, currentValue);
setIsEditing(false);
setStatus('idle');
} catch (err) {
setStatus('error');
setError(err.message || 'Salvataggio fallito');
} }
setIsEditing(false);
}; };
const handleKeyDown = (e) => { const handleCancel = () => {
if (e.key === 'Enter') { setCurrentValue(value);
handleSave(); setIsEditing(false);
} else if (e.key === 'Escape') { setStatus('idle');
// Annulla le modifiche e chiudi setError('');
setCurrentValue(value);
setIsEditing(false);
}
}; };
const renderInputField = () => { const renderInputField = () => {
@@ -42,38 +51,48 @@ function EditableField({
name, name,
value: currentValue || '', value: currentValue || '',
onChange: (e) => setCurrentValue(e.target.value), onChange: (e) => setCurrentValue(e.target.value),
onBlur: handleSave,
onKeyDown: handleKeyDown,
autoFocus: true, autoFocus: true,
}; };
if (type === 'select') { if (type === 'select') {
return ( return <select {...commonProps}>{options.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}</select>;
<select {...commonProps}>
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
);
} }
if (type === 'textarea') { if (type === 'textarea') {
return <textarea {...commonProps} rows="5" />; return <textarea {...commonProps} rows="5" />;
} }
// Default per 'text', 'number', 'date', etc.
return <input type={type} {...commonProps} />; return <input type={type} {...commonProps} />;
}; };
const renderDisplayValue = () => {
if (type === 'url' && value) {
return <a href={value} target="_blank" rel="noopener noreferrer">{value}</a>;
}
return <span className="display-value">{value || <em>N/A</em>}</span>;
};
return ( return (
<div className="editable-field" onClick={() => !isEditing && setIsEditing(true)}> <div className="editable-field-container">
<span className="field-label">{label}</span> <label>{label}</label>
{isEditing ? ( {isEditing ? (
renderInputField() <div className="edit-mode">
{renderInputField()}
<div className="edit-actions">
<button onClick={handleSave} disabled={status === 'saving'} className="button-icon">
{status === 'saving' ? '...' : <SaveIcon />}
</button>
<button onClick={handleCancel} disabled={status === 'saving'} className="button-icon secondary">
<CancelIcon />
</button>
</div>
{status === 'error' && <small className="error-text">{error}</small>}
</div>
) : ( ) : (
<span className="field-value">{value || 'N/A'}</span> <div className="display-mode">
{renderDisplayValue()}
<button onClick={() => setIsEditing(true)} className="button-icon outline">
<EditIcon />
</button>
</div>
)} )}
</div> </div>
); );