improve profile
This commit is contained in:
@@ -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
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: '',
|
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>
|
||||||
|
|||||||
@@ -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 { 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
// 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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user