improve profile
This commit is contained in:
@@ -35,6 +35,12 @@ function CelebrityProfile() {
|
||||
const handleFieldSave = async (fieldName, newValue) => {
|
||||
// Converte stringa vuota a null per il backend
|
||||
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 };
|
||||
|
||||
try {
|
||||
|
||||
249
gemini-schema.json
Normal file
249
gemini-schema.json
Normal 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"
|
||||
]
|
||||
}
|
||||
@@ -8,9 +8,11 @@ const initialFormState = {
|
||||
birth_date: '',
|
||||
height_cm: '',
|
||||
weight_kg: '',
|
||||
body_type: '',
|
||||
bust_cm: '',
|
||||
waist_cm: '',
|
||||
hips_cm: '',
|
||||
chest_circumference_cm: '',
|
||||
bra_band_size: '',
|
||||
bra_cup_size: '',
|
||||
bra_size_system: 'US',
|
||||
@@ -22,6 +24,9 @@ const initialFormState = {
|
||||
hair_color: '',
|
||||
eye_color: '',
|
||||
biography: '',
|
||||
shoe_size: '',
|
||||
shoe_size_system: 'EU',
|
||||
official_website: '',
|
||||
};
|
||||
|
||||
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>Nazionalità<input type="text" name="nationality" value={formData.nationality} onChange={handleChange} /></label>
|
||||
</div>
|
||||
<label>
|
||||
Sito Ufficiale
|
||||
<input type="url" name="official_website" value={formData.official_website} onChange={handleChange} placeholder="https://..."/>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
@@ -91,11 +100,23 @@ function CelebrityCreate() {
|
||||
<div className="grid">
|
||||
<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>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 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>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 Occhi<input type="text" name="eye_color" value={formData.eye_color} onChange={handleChange} /></label>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { getCelebrityById, updateCelebrity, deleteCelebrity } from '../services/api';
|
||||
import EditableField from './EditableField';
|
||||
import './CelebrityProfile.css'; // Importa il nuovo CSS per il layout del profilo
|
||||
|
||||
function CelebrityProfile() {
|
||||
const { id } = useParams();
|
||||
@@ -25,100 +26,116 @@ function CelebrityProfile() {
|
||||
.catch((err) => setError(err.message))
|
||||
.finally(() => setLoading(false));
|
||||
} else {
|
||||
navigate('/');
|
||||
navigate('/');
|
||||
}
|
||||
}, [id, navigate]);
|
||||
|
||||
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 };
|
||||
|
||||
try {
|
||||
await updateCelebrity(id, payload);
|
||||
const updatedCelebrity = await updateCelebrity(id, payload);
|
||||
// Aggiorna lo stato locale per un feedback immediato
|
||||
setCelebrity((prev) => ({ ...prev, [fieldName]: newValue }));
|
||||
console.log('Salvataggio riuscito:', updatedCelebrity);
|
||||
} 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 () => {
|
||||
if (window.confirm(`Sei sicuro di voler eliminare ${celebrity.name}? L'azione è irreversibile.`)) {
|
||||
try {
|
||||
await deleteCelebrity(id);
|
||||
navigate('/');
|
||||
} catch (err) {
|
||||
setError(`Errore durante l'eliminazione: ${err.message}`);
|
||||
}
|
||||
try {
|
||||
await deleteCelebrity(id);
|
||||
alert(`${celebrity.name} è stato eliminato con successo.`);
|
||||
navigate('/');
|
||||
} catch (err) {
|
||||
setError(`Errore durante l'eliminazione: ${err.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
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>;
|
||||
|
||||
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 booleanOptions = [ { value: 'true', label: 'Sì' }, { value: 'false', label: 'No' }];
|
||||
|
||||
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 shoeSystemOptions = [{ value: 'EU', label: 'EU' }, { value: 'US', label: 'US' }, { value: 'UK', label: 'UK' }];
|
||||
const booleanOptions = [{ value: 'true', label: 'Sì' }, { value: 'false', label: 'No' }];
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<aside>
|
||||
<div className="profile-container">
|
||||
<aside className="profile-sidebar">
|
||||
<figure>
|
||||
<img src={`https://i.pravatar.cc/400?u=${id}`} alt={celebrity.name} style={{ width: '100%' }} />
|
||||
<figcaption>{celebrity.name}</figcaption>
|
||||
<img src={`https://i.pravatar.cc/400?u=${id}`} alt={celebrity.name} />
|
||||
<figcaption>
|
||||
<EditableField label="Nome" name="name" value={celebrity.name} onSave={handleFieldSave} />
|
||||
</figcaption>
|
||||
</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>
|
||||
|
||||
<section>
|
||||
<article>
|
||||
<header>
|
||||
<h2>{celebrity.name}</h2>
|
||||
<EditableField label="Nome completo / Alias" name="name" value={celebrity.name} onSave={handleFieldSave} />
|
||||
</header>
|
||||
|
||||
<h3>Dati Personali</h3>
|
||||
<div className="grid">
|
||||
<main className="profile-content">
|
||||
{error && <p className="error-message">Errore: {error}</p>}
|
||||
|
||||
<details open>
|
||||
<summary><h3>Dati Personali</h3></summary>
|
||||
<div className="profile-grid">
|
||||
<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="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="Genere" name="gender" value={celebrity.gender} type="select" options={genderOptions} onSave={handleFieldSave} />
|
||||
<EditableField label="Sessualità" name="sexuality" value={celebrity.sexuality} onSave={handleFieldSave} />
|
||||
</div>
|
||||
<hr/>
|
||||
<h3>Aspetto Fisico</h3>
|
||||
<div className="grid">
|
||||
<EditableField label="Sito Ufficiale" name="official_website" value={celebrity.official_website} type="url" onSave={handleFieldSave} />
|
||||
</details>
|
||||
|
||||
<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="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 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="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} />
|
||||
{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>
|
||||
<hr/>
|
||||
<h3>Reggiseno</h3>
|
||||
<div className="grid">
|
||||
<EditableField label="Misura Fascia" name="bra_band_size" value={celebrity.bra_band_size} type="number" onSave={handleFieldSave} />
|
||||
<EditableField label="Coppa" name="bra_cup_size" value={celebrity.bra_cup_size} onSave={handleFieldSave} />
|
||||
<EditableField label="Sistema" name="bra_size_system" value={celebrity.bra_size_system} type="select" options={braSystemOptions} onSave={handleFieldSave} />
|
||||
<EditableField label="Seno Naturale?" name="boobs_are_natural" value={String(celebrity.boobs_are_natural)} type="select" options={booleanOptions} onSave={handleFieldSave} />
|
||||
</div>
|
||||
<hr/>
|
||||
<h3>Biografia</h3>
|
||||
<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>
|
||||
</details>
|
||||
|
||||
{celebrity.gender !== 'male' && (
|
||||
<details>
|
||||
<summary><h3>Reggiseno</h3></summary>
|
||||
<div className="profile-grid">
|
||||
<EditableField label="Misura Fascia" name="bra_band_size" value={celebrity.bra_band_size} type="number" onSave={handleFieldSave} />
|
||||
<EditableField label="Coppa" name="bra_cup_size" value={celebrity.bra_cup_size} onSave={handleFieldSave} />
|
||||
<EditableField label="Sistema" name="bra_size_system" value={celebrity.bra_size_system} type="select" options={braSystemOptions} onSave={handleFieldSave} />
|
||||
<EditableField label="Seno Naturale?" name="boobs_are_natural" value={String(celebrity.boobs_are_natural)} type="select" options={booleanOptions} onSave={handleFieldSave} />
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
</section>
|
||||
</details>
|
||||
)}
|
||||
|
||||
<details>
|
||||
<summary><h3>Biografia</h3></summary>
|
||||
<EditableField label="Note biografiche" name="biography" value={celebrity.biography} type="textarea" onSave={handleFieldSave} />
|
||||
</details>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
86
packages/frontend/src/components/EditableField.css
Normal file
86
packages/frontend/src/components/EditableField.css
Normal 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;
|
||||
}
|
||||
@@ -1,40 +1,49 @@
|
||||
// packages/frontend/src/components/EditableField.jsx
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import './EditableField.css'; // Importiamo il nuovo CSS
|
||||
|
||||
function EditableField({
|
||||
label,
|
||||
name,
|
||||
value,
|
||||
onSave,
|
||||
type = 'text',
|
||||
options = [], // Per i campi <select>
|
||||
}) {
|
||||
// Piccole icone SVG per i pulsanti
|
||||
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>;
|
||||
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>;
|
||||
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>;
|
||||
|
||||
|
||||
function EditableField({ label, name, value, onSave, type = 'text', options = [] }) {
|
||||
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 [status, setStatus] = useState('idle'); // 'idle', 'saving', 'error'
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Aggiorna il valore interno se il prop 'value' cambia dall'esterno
|
||||
useEffect(() => {
|
||||
setCurrentValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleSave = () => {
|
||||
// Salva solo se il valore è cambiato
|
||||
if (currentValue !== value) {
|
||||
onSave(name, currentValue);
|
||||
const handleSave = async () => {
|
||||
if (currentValue === value) {
|
||||
setIsEditing(false);
|
||||
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) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSave();
|
||||
} else if (e.key === 'Escape') {
|
||||
// Annulla le modifiche e chiudi
|
||||
setCurrentValue(value);
|
||||
setIsEditing(false);
|
||||
}
|
||||
const handleCancel = () => {
|
||||
setCurrentValue(value);
|
||||
setIsEditing(false);
|
||||
setStatus('idle');
|
||||
setError('');
|
||||
};
|
||||
|
||||
const renderInputField = () => {
|
||||
@@ -42,38 +51,48 @@ function EditableField({
|
||||
name,
|
||||
value: currentValue || '',
|
||||
onChange: (e) => setCurrentValue(e.target.value),
|
||||
onBlur: handleSave,
|
||||
onKeyDown: handleKeyDown,
|
||||
autoFocus: true,
|
||||
};
|
||||
|
||||
if (type === 'select') {
|
||||
return (
|
||||
<select {...commonProps}>
|
||||
{options.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
return <select {...commonProps}>{options.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}</select>;
|
||||
}
|
||||
|
||||
if (type === 'textarea') {
|
||||
return <textarea {...commonProps} rows="5" />;
|
||||
}
|
||||
|
||||
// Default per 'text', 'number', 'date', etc.
|
||||
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 (
|
||||
<div className="editable-field" onClick={() => !isEditing && setIsEditing(true)}>
|
||||
<span className="field-label">{label}</span>
|
||||
<div className="editable-field-container">
|
||||
<label>{label}</label>
|
||||
{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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user