diff --git a/.env b/.env index 824e709..1885d55 100644 --- a/.env +++ b/.env @@ -1,11 +1,12 @@ # ============== POSTGRES ============== +POSTGRES_SERVER=db POSTGRES_DB=mydatabase POSTGRES_USER=myuser POSTGRES_PASSWORD=mysecretpassword # URL del database per SQLAlchemy e Alembic # La porta 5432 è quella interna di Docker. 'db' è il nome del servizio in docker-compose. -DATABASE_URL=postgresql://myuser:mysecretpassword@db:5432/mydatabase +DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_SERVER}/${POSTGRES_DB} # ============== FRONTEND ============== # L'URL a cui il frontend contatterà il backend (attraverso il proxy) diff --git a/ackages/frontend/src/components/CelebrityProfile.jsx b/ackages/frontend/src/components/CelebrityProfile.jsx new file mode 100644 index 0000000..a6a96e6 --- /dev/null +++ b/ackages/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 + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + +
+

Misure e Aspetto

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

Reggiseno

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + {error &&

Errore: {error}

} + +
+ Annulla + +
+ + + ); +} + +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à + + + + + + + + + + + + + + + {celebrities.map((celeb) => ( + + + + + + + + + + ))} + +
NomeSeno (cm)Vita (cm)Fianchi (cm)Taglia ReggisenoNaturali?Azioni
{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 +}) { + 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); + + // 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); + } + 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 renderInputField = () => { + const commonProps = { + name, + value: currentValue || '', + onChange: (e) => setCurrentValue(e.target.value), + onBlur: handleSave, + onKeyDown: handleKeyDown, + autoFocus: true, + }; + + if (type === 'select') { + return ( + + ); + } + + if (type === 'textarea') { + return