-
Monorepo React + FastAPI
-
Messaggio dal backend: {message}
+
+
+ } />
+ {/* 2. Riattiva questa rotta e puntala al nuovo componente */}
+ } />
+ } />
+
);
}
diff --git a/packages/frontend/src/components/CelebrityCreate.css b/packages/frontend/src/components/CelebrityCreate.css
new file mode 100644
index 0000000..9384dee
--- /dev/null
+++ b/packages/frontend/src/components/CelebrityCreate.css
@@ -0,0 +1,110 @@
+/* packages/frontend/src/components/CelebrityCreate.css */
+
+.create-form-container {
+ width: 100%;
+ max-width: 900px;
+ margin: 2rem auto;
+ padding: 2rem;
+ background-color: #2c2c2c;
+ border-radius: 8px;
+ border: 1px solid #444;
+}
+
+.create-form-container h2 {
+ margin-top: 0;
+ color: #d4b996;
+}
+
+.form-section {
+ margin-bottom: 2rem;
+ border-top: 1px solid #444;
+ padding-top: 1.5rem;
+}
+
+.form-section h3 {
+ margin-top: 0;
+ margin-bottom: 1rem;
+}
+
+.form-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 1.5rem;
+}
+
+.form-group {
+ display: flex;
+ flex-direction: column;
+}
+
+.form-group label {
+ margin-bottom: 0.5rem;
+ font-size: 0.9rem;
+ color: #ccc;
+}
+
+.form-group input,
+.form-group select,
+.form-group textarea {
+ padding: 0.75rem;
+ border-radius: 4px;
+ border: 1px solid #555;
+ background-color: #1e1e1e;
+ color: #f0f0f0;
+ font-size: 1rem;
+}
+
+.form-group input:focus,
+.form-group select:focus {
+ outline: none;
+ border-color: #646cff;
+ box-shadow: 0 0 0 2px rgba(100, 108, 255, 0.5);
+}
+
+.form-group-checkbox {
+ margin-top: 1rem;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.form-group-checkbox input {
+ width: 18px;
+ height: 18px;
+}
+
+.form-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 1rem;
+ margin-top: 2rem;
+ border-top: 1px solid #444;
+ padding-top: 1.5rem;
+}
+
+.button-save {
+ background-color: #28a745;
+}
+.button-save:hover {
+ background-color: #218838;
+ border-color: #1e7e34;
+}
+.button-save:disabled {
+ background-color: #555;
+ cursor: not-allowed;
+}
+
+.button-cancel {
+ background-color: #6c757d;
+ padding: 0.6em 1.2em; /* Emula lo stile del bottone */
+ border-radius: 8px;
+ font-weight: 500;
+ text-decoration: none;
+ color: white;
+ display: inline-block;
+ border: 1px solid transparent;
+}
+.button-cancel:hover {
+ background-color: #5a6268;
+ border-color: #545b62;
+}
\ No newline at end of file
diff --git a/packages/frontend/src/components/CelebrityCreate.jsx b/packages/frontend/src/components/CelebrityCreate.jsx
new file mode 100644
index 0000000..7690e02
--- /dev/null
+++ b/packages/frontend/src/components/CelebrityCreate.jsx
@@ -0,0 +1,177 @@
+// packages/frontend/src/components/CelebrityCreate.jsx
+
+import React, { useState } from 'react'; // <-- ECCO LA CORREZIONE
+import { useNavigate, Link } from 'react-router-dom';
+import { createCelebrity } from '../services/api';
+import './CelebrityCreate.css'; // Stile dedicato per questo form
+
+// Stato iniziale pulito per il form
+const initialFormState = {
+ name: '',
+ gender: 'female',
+ birth_date: '',
+ height_cm: '',
+ weight_kg: '',
+ bust_cm: '',
+ waist_cm: '',
+ hips_cm: '',
+ bra_band_size: '',
+ bra_cup_size: '',
+ bra_size_system: 'US',
+ boobs_are_natural: true,
+ birth_place: '',
+ nationality: '',
+ ethnicity: '',
+ sexuality: '',
+ hair_color: '',
+ eye_color: '',
+ biography: '',
+};
+
+function CelebrityCreate() {
+ const navigate = useNavigate();
+ const [formData, setFormData] = useState(initialFormState);
+ const [error, setError] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const handleChange = (e) => {
+ const { name, value, type, checked } = e.target;
+ setFormData({
+ ...formData,
+ [name]: type === 'checkbox' ? checked : value,
+ });
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setError(null);
+ setIsSubmitting(true);
+
+ // "Pulisce" i dati: converte le stringhe vuote dei campi numerici in `null`
+ // per evitare errori di validazione nel backend.
+ const payload = { ...formData };
+ Object.keys(payload).forEach(key => {
+ if (payload[key] === '') {
+ payload[key] = null;
+ }
+ });
+
+ try {
+ await createCelebrity(payload);
+ alert('Celebrità creata con successo!');
+ navigate('/'); // Torna alla lista dopo la creazione
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+
Aggiungi Nuova Celebrità
+
Compila i campi sottostanti per aggiungere un nuovo profilo al catalogo.
+
+
+ );
+}
+
+export default CelebrityCreate;
\ No newline at end of file
diff --git a/packages/frontend/src/components/CelebrityList.jsx b/packages/frontend/src/components/CelebrityList.jsx
new file mode 100644
index 0000000..ab89056
--- /dev/null
+++ b/packages/frontend/src/components/CelebrityList.jsx
@@ -0,0 +1,85 @@
+import React, { useState, useEffect } from 'react';
+import { Link } from 'react-router-dom';
+import { getCelebrities, deleteCelebrity } from '../services/api';
+
+function CelebrityList() {
+ const [celebrities, setCelebrities] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ fetchCelebrities();
+ }, []);
+
+ const fetchCelebrities = async () => {
+ try {
+ setLoading(true);
+ const data = await getCelebrities();
+ setCelebrities(data);
+ setError(null);
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleDelete = async (id) => {
+ if (window.confirm('Sei sicuro di voler eliminare questa celebrità?')) {
+ try {
+ await deleteCelebrity(id);
+ // Rimuovi la celebrità dalla lista senza ricaricare i dati
+ setCelebrities(celebrities.filter((c) => c.id !== id));
+ } catch (err) {
+ alert(`Errore: ${err.message}`);
+ }
+ }
+ };
+
+ if (loading) return
Caricamento in corso...
;
+ if (error) return
Errore: {error}
;
+
+ return (
+
+
Catalogo Celebrità
+
+ + Aggiungi Nuova Celebrità
+
+
+
+
+ | Nome |
+ Seno (cm) |
+ Vita (cm) |
+ Fianchi (cm) |
+ Taglia Reggiseno |
+ Naturali? |
+ Azioni |
+
+
+
+ {celebrities.map((celeb) => (
+
+ | {celeb.name} |
+ {celeb.bust_cm || 'N/A'} |
+ {celeb.waist_cm || 'N/A'} |
+ {celeb.hips_cm || 'N/A'} |
+
+ {celeb.bra_band_size && celeb.bra_cup_size
+ ? `${celeb.bra_band_size}${celeb.bra_cup_size} (${celeb.bra_size_system})`
+ : 'N/A'}
+ |
+ {celeb.boobs_are_natural === null ? 'N/A' : celeb.boobs_are_natural ? 'Sì' : 'No'} |
+
+ Modifica
+
+ |
+
+ ))}
+
+
+
+ );
+}
+
+export default CelebrityList;
\ No newline at end of file
diff --git a/packages/frontend/src/components/CelebrityProfile.css b/packages/frontend/src/components/CelebrityProfile.css
new file mode 100644
index 0000000..d80c056
--- /dev/null
+++ b/packages/frontend/src/components/CelebrityProfile.css
@@ -0,0 +1,138 @@
+/* packages/frontend/src/components/CelebrityProfile.css */
+
+.profile-container {
+ display: flex;
+ gap: 2rem;
+ width: 100%;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.profile-sidebar {
+ flex: 0 0 300px; /* Larghezza fissa per la colonna sinistra */
+}
+
+.profile-main-image {
+ width: 100%;
+ height: auto;
+ border-radius: 8px;
+ margin-bottom: 1rem;
+ border: 1px solid #444;
+}
+
+.thumbnails {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 0.5rem;
+}
+
+.thumbnail img {
+ width: 100%;
+ border-radius: 4px;
+ border: 1px solid #444;
+}
+
+.profile-content {
+ flex-grow: 1;
+}
+
+.profile-header {
+ border-bottom: 1px solid #555;
+ padding-bottom: 1rem;
+ margin-bottom: 1rem;
+}
+
+.profile-header h1 {
+ margin: 0;
+ font-size: 2.5rem;
+ color: #f0e6d2;
+}
+
+.profile-header .editable-field {
+ margin-top: 0.5rem;
+}
+
+.profile-section {
+ background-color: #2c2c2c;
+ border-radius: 8px;
+ padding: 1.5rem;
+ margin-bottom: 1.5rem;
+ border: 1px solid #444;
+}
+
+.profile-section h2 {
+ margin-top: 0;
+ border-bottom: 1px solid #555;
+ padding-bottom: 0.5rem;
+ margin-bottom: 1rem;
+ color: #d4b996;
+}
+
+.profile-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1rem;
+}
+
+/* Stile per i campi editabili */
+.editable-field {
+ padding: 8px;
+ border-radius: 4px;
+ transition: background-color 0.2s ease-in-out;
+ cursor: pointer;
+}
+
+.editable-field:hover {
+ background-color: #3a3a3a;
+}
+
+.field-label {
+ display: block;
+ font-size: 0.8rem;
+ color: #aaa;
+ margin-bottom: 4px;
+ text-transform: uppercase;
+}
+
+.field-value {
+ font-size: 1rem;
+ font-weight: 500;
+ color: #fff;
+}
+
+.editable-field input,
+.editable-field select,
+.editable-field textarea {
+ width: 100%;
+ padding: 6px;
+ font-size: 1rem;
+ border: 1px solid #646cff;
+ border-radius: 4px;
+ background-color: #1a1a1a;
+ color: #fff;
+ box-sizing: border-box; /* Assicura che padding non alteri la larghezza */
+}
+
+.profile-actions {
+ margin-top: 2rem;
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+}
+
+.button-delete {
+ background-color: #b22222;
+ color: white;
+}
+.button-delete:hover {
+ background-color: #dc143c;
+ border-color: #dc143c;
+}
+
+.button-back {
+ background-color: #444;
+}
+.button-back:hover {
+ background-color: #555;
+ border-color: #666;
+}
\ No newline at end of file
diff --git a/packages/frontend/src/components/CelebrityProfile.jsx b/packages/frontend/src/components/CelebrityProfile.jsx
new file mode 100644
index 0000000..a6a96e6
--- /dev/null
+++ b/packages/frontend/src/components/CelebrityProfile.jsx
@@ -0,0 +1,148 @@
+// packages/frontend/src/components/CelebrityProfile.jsx
+
+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
+
+function CelebrityProfile() {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ const [celebrity, setCelebrity] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ // Ignoriamo la modalità "new", questa pagina è solo per visualizzare/modificare
+ if (id) {
+ setLoading(true);
+ getCelebrityById(id)
+ .then((data) => {
+ // Formatta la data per l'input type="date"
+ if (data.birth_date) {
+ data.birth_date = data.birth_date.split('T')[0];
+ }
+ setCelebrity(data);
+ })
+ .catch((err) => setError(err.message))
+ .finally(() => setLoading(false));
+ } else {
+ navigate('/'); // Se non c'è id, torna alla home
+ }
+ }, [id, navigate]);
+
+ const handleFieldSave = async (fieldName, newValue) => {
+ // Converte stringa vuota a null per il backend
+ const valueToSend = newValue === '' ? null : newValue;
+ const payload = { [fieldName]: valueToSend };
+
+ try {
+ 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 del campo ${fieldName}: ${err.message}`);
+ // Potresti voler ripristinare il valore precedente in caso di errore
+ }
+ };
+
+ const handleDelete = async () => {
+ if (window.confirm(`Sei sicuro di voler eliminare ${celebrity.name}? L'azione è irreversibile.`)) {
+ try {
+ await deleteCelebrity(id);
+ alert(`${celebrity.name} è stato eliminato con successo.`);
+ navigate('/');
+ } catch (err) {
+ setError(`Errore durante l'eliminazione: ${err.message}`);
+ }
+ }
+ };
+
+ if (loading) return
Caricamento profilo...
;
+ if (error) return
Errore: {error}
;
+ if (!celebrity) return
Nessuna celebrità trovata.
;
+
+ // Opzioni per i campi