aliases
This commit is contained in:
Binary file not shown.
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.orm import Session, joinedload
|
from sqlalchemy.orm import Session, joinedload
|
||||||
from . import models, schemas
|
from . import models, schemas
|
||||||
|
|
||||||
@@ -6,11 +7,25 @@ def get_celebrity(db: Session, celebrity_id: int):
|
|||||||
# Usiamo joinedload per caricare in anticipo le relazioni e evitare query N+1
|
# Usiamo joinedload per caricare in anticipo le relazioni e evitare query N+1
|
||||||
return db.query(models.Celebrity).options(
|
return db.query(models.Celebrity).options(
|
||||||
joinedload(models.Celebrity.profile_image),
|
joinedload(models.Celebrity.profile_image),
|
||||||
joinedload(models.Celebrity.images)
|
joinedload(models.Celebrity.images),
|
||||||
|
joinedload(models.Celebrity.aliases) # Carica anche gli alias
|
||||||
).filter(models.Celebrity.id == celebrity_id).first()
|
).filter(models.Celebrity.id == celebrity_id).first()
|
||||||
|
|
||||||
def get_celebrities(db: Session, skip: int = 0, limit: int = 100):
|
def get_celebrities(db: Session, skip: int = 0, limit: int = 100, search: str = None):
|
||||||
return db.query(models.Celebrity).offset(skip).limit(limit).all()
|
query = db.query(models.Celebrity).options(
|
||||||
|
joinedload(models.Celebrity.profile_image)
|
||||||
|
)
|
||||||
|
if search:
|
||||||
|
search_term = f"%{search}%"
|
||||||
|
# Esegue un join con la tabella degli alias e filtra per nome o per alias
|
||||||
|
query = query.outerjoin(models.Celebrity.aliases).filter(
|
||||||
|
sa.or_(
|
||||||
|
models.Celebrity.name.ilike(search_term),
|
||||||
|
models.CelebrityAlias.alias_name.ilike(search_term)
|
||||||
|
)
|
||||||
|
).distinct() # distinct() evita duplicati se una celebrità ha più alias che matchano
|
||||||
|
|
||||||
|
return query.offset(skip).limit(limit).all()
|
||||||
|
|
||||||
def create_celebrity(db: Session, celebrity: schemas.CelebrityCreate):
|
def create_celebrity(db: Session, celebrity: schemas.CelebrityCreate):
|
||||||
db_celebrity = models.Celebrity(**celebrity.model_dump())
|
db_celebrity = models.Celebrity(**celebrity.model_dump())
|
||||||
@@ -41,6 +56,23 @@ def delete_celebrity(db: Session, celebrity_id: int):
|
|||||||
db.commit()
|
db.commit()
|
||||||
return db_celebrity
|
return db_celebrity
|
||||||
|
|
||||||
|
# --- Funzioni CRUD per gli Alias ---
|
||||||
|
|
||||||
|
def create_celebrity_alias(db: Session, celebrity_id: int, alias: schemas.CelebrityAliasCreate):
|
||||||
|
db_alias = models.CelebrityAlias(celebrity_id=celebrity_id, alias_name=alias.alias_name)
|
||||||
|
db.add(db_alias)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_alias)
|
||||||
|
return db_alias
|
||||||
|
|
||||||
|
def delete_celebrity_alias(db: Session, alias_id: int):
|
||||||
|
db_alias = db.query(models.CelebrityAlias).filter(models.CelebrityAlias.id == alias_id).first()
|
||||||
|
if not db_alias:
|
||||||
|
return None
|
||||||
|
db.delete(db_alias)
|
||||||
|
db.commit()
|
||||||
|
return db_alias
|
||||||
|
|
||||||
# --- Funzioni CRUD per le Immagini ---
|
# --- Funzioni CRUD per le Immagini ---
|
||||||
|
|
||||||
def get_image(db: Session, image_id: int):
|
def get_image(db: Session, image_id: int):
|
||||||
|
|||||||
Binary file not shown.
@@ -1,8 +1,9 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, File, UploadFile
|
from fastapi import APIRouter, Depends, HTTPException, File, UploadFile, Response
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import List
|
from typing import List, Optional
|
||||||
import uuid
|
import uuid
|
||||||
import shutil
|
import shutil
|
||||||
|
import httpx
|
||||||
|
|
||||||
from .. import crud, models, schemas
|
from .. import crud, models, schemas
|
||||||
from ..database import get_db
|
from ..database import get_db
|
||||||
@@ -15,12 +16,12 @@ router = APIRouter(
|
|||||||
|
|
||||||
@router.post("/", response_model=schemas.Celebrity, status_code=201)
|
@router.post("/", response_model=schemas.Celebrity, status_code=201)
|
||||||
def create_celebrity(celebrity: schemas.CelebrityCreate, db: Session = Depends(get_db)):
|
def create_celebrity(celebrity: schemas.CelebrityCreate, db: Session = Depends(get_db)):
|
||||||
# Qui potresti aggiungere un check per vedere se una celebrità con lo stesso nome esiste già
|
# Qui potresti aggiungere un check per vedere se una celebrità con lo stesso nome esiste già
|
||||||
return crud.create_celebrity(db=db, celebrity=celebrity)
|
return crud.create_celebrity(db=db, celebrity=celebrity)
|
||||||
|
|
||||||
@router.get("/", response_model=List[schemas.Celebrity])
|
@router.get("/", response_model=List[schemas.Celebrity])
|
||||||
def read_celebrities(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
def read_celebrities(skip: int = 0, limit: int = 100, search: Optional[str] = None, db: Session = Depends(get_db)):
|
||||||
celebrities = crud.get_celebrities(db, skip=skip, limit=limit)
|
celebrities = crud.get_celebrities(db, skip=skip, limit=limit, search=search)
|
||||||
return celebrities
|
return celebrities
|
||||||
|
|
||||||
@router.get("/{celebrity_id}", response_model=schemas.Celebrity)
|
@router.get("/{celebrity_id}", response_model=schemas.Celebrity)
|
||||||
@@ -44,7 +45,23 @@ def delete_celebrity(celebrity_id: int, db: Session = Depends(get_db)):
|
|||||||
raise HTTPException(status_code=404, detail="Celebrity not found")
|
raise HTTPException(status_code=404, detail="Celebrity not found")
|
||||||
return db_celebrity
|
return db_celebrity
|
||||||
|
|
||||||
# --- NUOVI ENDPOINT PER LE IMMAGINI ---
|
# --- NUOVI ENDPOINT PER GLI ALIAS ---
|
||||||
|
|
||||||
|
@router.post("/{celebrity_id}/aliases", response_model=schemas.CelebrityAlias, status_code=201)
|
||||||
|
def add_alias_to_celebrity(celebrity_id: int, alias: schemas.CelebrityAliasCreate, db: Session = Depends(get_db)):
|
||||||
|
db_celebrity = crud.get_celebrity(db, celebrity_id=celebrity_id)
|
||||||
|
if db_celebrity is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Celebrity not found")
|
||||||
|
return crud.create_celebrity_alias(db=db, celebrity_id=celebrity_id, alias=alias)
|
||||||
|
|
||||||
|
@router.delete("/aliases/{alias_id}", status_code=204)
|
||||||
|
def delete_alias(alias_id: int, db: Session = Depends(get_db)):
|
||||||
|
db_alias = crud.delete_celebrity_alias(db, alias_id=alias_id)
|
||||||
|
if db_alias is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Alias not found")
|
||||||
|
return Response(status_code=204)
|
||||||
|
|
||||||
|
# --- ENDPOINT PER LE IMMAGINI ---
|
||||||
|
|
||||||
@router.post("/{celebrity_id}/images", response_model=List[schemas.Image], status_code=201)
|
@router.post("/{celebrity_id}/images", response_model=List[schemas.Image], status_code=201)
|
||||||
def upload_celebrity_images(celebrity_id: int, files: List[UploadFile] = File(...), db: Session = Depends(get_db)):
|
def upload_celebrity_images(celebrity_id: int, files: List[UploadFile] = File(...), db: Session = Depends(get_db)):
|
||||||
@@ -69,9 +86,6 @@ def upload_celebrity_images(celebrity_id: int, files: List[UploadFile] = File(..
|
|||||||
created_images.append(db_image)
|
created_images.append(db_image)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Se un file fallisce, potremmo voler continuare con gli altri,
|
|
||||||
# ma per ora lanciamo un'eccezione per l'intero batch.
|
|
||||||
# In un'app di produzione, si potrebbe restituire una risposta parziale.
|
|
||||||
print(f"Failed to upload file {file.filename}: {e}")
|
print(f"Failed to upload file {file.filename}: {e}")
|
||||||
raise HTTPException(status_code=500, detail=f"Could not upload file: {file.filename}")
|
raise HTTPException(status_code=500, detail=f"Could not upload file: {file.filename}")
|
||||||
|
|
||||||
@@ -89,7 +103,6 @@ async def add_image_from_url(celebrity_id: int, request: schemas.ImageUrlRequest
|
|||||||
url = str(request.url)
|
url = str(request.url)
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
# Eseguiamo prima una richiesta HEAD per controllare il tipo e la dimensione
|
|
||||||
head_response = await client.head(url, follow_redirects=True, timeout=10)
|
head_response = await client.head(url, follow_redirects=True, timeout=10)
|
||||||
head_response.raise_for_status()
|
head_response.raise_for_status()
|
||||||
|
|
||||||
@@ -97,16 +110,13 @@ async def add_image_from_url(celebrity_id: int, request: schemas.ImageUrlRequest
|
|||||||
if not content_type or not content_type.startswith('image/'):
|
if not content_type or not content_type.startswith('image/'):
|
||||||
raise HTTPException(status_code=400, detail="URL does not point to a valid image.")
|
raise HTTPException(status_code=400, detail="URL does not point to a valid image.")
|
||||||
|
|
||||||
# Limite di dimensione (es. 10MB)
|
|
||||||
content_length = int(head_response.headers.get('content-length', 0))
|
content_length = int(head_response.headers.get('content-length', 0))
|
||||||
if content_length > 10 * 1024 * 1024:
|
if content_length > 10 * 1024 * 1024:
|
||||||
raise HTTPException(status_code=400, detail="Image size exceeds 10MB limit.")
|
raise HTTPException(status_code=400, detail="Image size exceeds 10MB limit.")
|
||||||
|
|
||||||
# Scarica l'immagine
|
|
||||||
get_response = await client.get(url, follow_redirects=True, timeout=30)
|
get_response = await client.get(url, follow_redirects=True, timeout=30)
|
||||||
get_response.raise_for_status()
|
get_response.raise_for_status()
|
||||||
|
|
||||||
# Determina l'estensione del file
|
|
||||||
file_extension = content_type.split('/')[-1]
|
file_extension = content_type.split('/')[-1]
|
||||||
if file_extension == 'jpeg': file_extension = 'jpg'
|
if file_extension == 'jpeg': file_extension = 'jpg'
|
||||||
|
|
||||||
@@ -130,8 +140,6 @@ def set_celebrity_profile_image(celebrity_id: int, request: schemas.SetProfileIm
|
|||||||
raise HTTPException(status_code=404, detail="Celebrity or Image not found, or image does not belong to celebrity.")
|
raise HTTPException(status_code=404, detail="Celebrity or Image not found, or image does not belong to celebrity.")
|
||||||
return updated_celebrity
|
return updated_celebrity
|
||||||
|
|
||||||
# NOTA: Un approccio pi├╣ RESTful sarebbe /api/images/{image_id} in un router dedicato.
|
|
||||||
# Per semplicità, lo inseriamo qui.
|
|
||||||
@router.delete("/images/{image_id}", response_model=schemas.Image)
|
@router.delete("/images/{image_id}", response_model=schemas.Image)
|
||||||
def delete_image(image_id: int, db: Session = Depends(get_db)):
|
def delete_image(image_id: int, db: Session = Depends(get_db)):
|
||||||
db_image = crud.delete_image(db, image_id=image_id)
|
db_image = crud.delete_image(db, image_id=image_id)
|
||||||
|
|||||||
@@ -1,36 +1,43 @@
|
|||||||
// packages/frontend/src/components/CelebrityList.jsx
|
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { getCelebrities, deleteCelebrity } from '../services/api';
|
import { getCelebrities, deleteCelebrity } from '../services/api';
|
||||||
import './CelebrityList.css'; // Importa il nuovo CSS
|
import './CelebrityList.css';
|
||||||
|
|
||||||
// Icone SVG semplici per le azioni
|
|
||||||
const EditIcon = () => <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 EditIcon = () => <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 DeleteIcon = () => <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>;
|
const DeleteIcon = () => <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>;
|
||||||
const MoreIcon = () => <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="1"></circle><circle cx="19" cy="12" r="1"></circle><circle cx="5" cy="12" r="1"></circle></svg>;
|
const MoreIcon = () => <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="1"></circle><circle cx="19" cy="12" r="1"></circle><circle cx="5" cy="12" r="1"></circle></svg>;
|
||||||
|
|
||||||
|
|
||||||
function CelebrityList() {
|
function CelebrityList() {
|
||||||
const [celebrities, setCelebrities] = useState([]);
|
const [celebrities, setCelebrities] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm);
|
||||||
// Stato per la paginazione
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Effetto per "debouncare" il termine di ricerca
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedSearchTerm(searchTerm);
|
||||||
|
}, 300); // Attende 300ms dopo l'ultima digitazione prima di aggiornare
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [searchTerm]);
|
||||||
|
|
||||||
|
// Effetto per caricare i dati quando il termine di ricerca (debounced) cambia
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCelebrities();
|
fetchCelebrities();
|
||||||
}, []);
|
}, [debouncedSearchTerm]);
|
||||||
|
|
||||||
const fetchCelebrities = async () => {
|
const fetchCelebrities = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = await getCelebrities();
|
const data = await getCelebrities(debouncedSearchTerm);
|
||||||
setCelebrities(data);
|
setCelebrities(data);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -51,24 +58,18 @@ function CelebrityList() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Memoizzazione dei dati filtrati e paginati per performance
|
// La paginazione ora opera sulla lista (già filtrata) ricevuta dal backend
|
||||||
const filteredCelebrities = useMemo(() => {
|
|
||||||
return celebrities.filter(c =>
|
|
||||||
c.name.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
);
|
|
||||||
}, [celebrities, searchTerm]);
|
|
||||||
|
|
||||||
const paginatedCelebrities = useMemo(() => {
|
const paginatedCelebrities = useMemo(() => {
|
||||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
return filteredCelebrities.slice(startIndex, startIndex + itemsPerPage);
|
return celebrities.slice(startIndex, startIndex + itemsPerPage);
|
||||||
}, [filteredCelebrities, currentPage, itemsPerPage]);
|
}, [celebrities, currentPage, itemsPerPage]);
|
||||||
|
|
||||||
const totalPages = Math.ceil(filteredCelebrities.length / itemsPerPage);
|
const totalPages = Math.ceil(celebrities.length / itemsPerPage);
|
||||||
|
|
||||||
const renderPagination = () => (
|
const renderPagination = () => (
|
||||||
<div className="pagination-controls">
|
<div className="pagination-controls">
|
||||||
<span>
|
<span>
|
||||||
Pagina {currentPage} di {totalPages}
|
Pagina {currentPage} di {totalPages} ({celebrities.length} risultati)
|
||||||
</span>
|
</span>
|
||||||
<div className="grid">
|
<div className="grid">
|
||||||
<button onClick={() => setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1}>
|
<button onClick={() => setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1}>
|
||||||
@@ -89,8 +90,8 @@ function CelebrityList() {
|
|||||||
const renderEmptyState = () => (
|
const renderEmptyState = () => (
|
||||||
<div className="empty-state">
|
<div className="empty-state">
|
||||||
<h3>Nessun Profilo Trovato</h3>
|
<h3>Nessun Profilo Trovato</h3>
|
||||||
<p>La tua lista è vuota. Inizia aggiungendo una nuova celebrità.</p>
|
<p>{searchTerm ? "Prova a modificare i termini della ricerca." : "Inizia aggiungendo una nuova celebrità."}</p>
|
||||||
<Link to="/celebrity/new" role="button" className="primary">+ Aggiungi la prima celebrità</Link>
|
<Link to="/celebrity/new" role="button" className="primary">+ Aggiungi una celebrità</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -106,18 +107,18 @@ function CelebrityList() {
|
|||||||
<div className="list-controls">
|
<div className="list-controls">
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Cerca per nome..."
|
placeholder="Cerca per nome o alias..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => { setSearchTerm(e.target.value); setCurrentPage(1); }}
|
onChange={(e) => { setSearchTerm(e.target.value); setCurrentPage(1); }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading && <article aria-busy="true">Caricamento in corso...</article>}
|
{loading && <article aria-busy="true">Ricerca in corso...</article>}
|
||||||
{error && <p className="error-message">Errore: {error}</p>}
|
{error && <p className="error-message">Errore: {error}</p>}
|
||||||
|
|
||||||
{!loading && !error && (
|
{!loading && !error && (
|
||||||
<>
|
<>
|
||||||
{celebrities.length === 0 ? renderEmptyState() : (
|
{paginatedCelebrities.length === 0 ? renderEmptyState() : (
|
||||||
<>
|
<>
|
||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
<table>
|
<table>
|
||||||
@@ -135,7 +136,7 @@ function CelebrityList() {
|
|||||||
{paginatedCelebrities.map((celeb) => (
|
{paginatedCelebrities.map((celeb) => (
|
||||||
<tr key={celeb.id}>
|
<tr key={celeb.id}>
|
||||||
<td>
|
<td>
|
||||||
<img src={`https://i.pravatar.cc/50?u=${celeb.id}`} alt={celeb.name} className="avatar-image" />
|
<img src={celeb.profile_image ? `/api/uploads/${celeb.profile_image.file_path}` : `https://i.pravatar.cc/50?u=${celeb.id}`} alt={celeb.name} className="avatar-image" />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Link to={`/celebrity/${celeb.id}`} className="celeb-link">
|
<Link to={`/celebrity/${celeb.id}`} className="celeb-link">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 400px; /* Altezza fissa per l'immagine profilo */
|
height: 400px; /* Altezza fissa per l'immagine profilo */
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
background-color: var(--pico-muted-background-color); /* Sfondo per immagini trasparenti o in caricamento */
|
background-color: var(--pico-muted-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-sidebar .editable-field-container {
|
.profile-sidebar .editable-field-container {
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.profile-sidebar .editable-field-container label {
|
.profile-sidebar .editable-field-container label {
|
||||||
display: none; /* Nasconde l'etichetta "Nome" sotto la foto */
|
display: none;
|
||||||
}
|
}
|
||||||
.profile-sidebar .display-value {
|
.profile-sidebar .display-value {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.profile-content {
|
.profile-content {
|
||||||
flex: 1 1 auto; /* Occupa lo spazio rimanente */
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +81,43 @@
|
|||||||
color: var(--pico-color-red-500);
|
color: var(--pico-color-red-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Stili per Sezione Alias --- */
|
||||||
|
.alias-section {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.alias-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 0 1.5rem 0;
|
||||||
|
border: 1px solid var(--pico-card-border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
.alias-list li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-bottom: 1px solid var(--pico-muted-border-color);
|
||||||
|
}
|
||||||
|
.alias-list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.alias-list li button {
|
||||||
|
padding: 0.1rem 0.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.alias-add-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
.alias-add-form input, .alias-add-form button {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Stili per Galleria e Upload --- */
|
/* --- Stili per Galleria e Upload --- */
|
||||||
.gallery-grid {
|
.gallery-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -135,7 +172,7 @@
|
|||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem; /* Aumentato lo spazio */
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
.upload-section h4 {
|
.upload-section h4 {
|
||||||
margin: 0 0 0.5rem 0;
|
margin: 0 0 0.5rem 0;
|
||||||
@@ -144,7 +181,7 @@
|
|||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
align-items: center; /* Allinea verticalmente */
|
align-items: center;
|
||||||
}
|
}
|
||||||
.upload-section input {
|
.upload-section input {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
// packages/frontend/src/components/CelebrityProfile.jsx
|
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||||
import { getCelebrityById, updateCelebrity, deleteCelebrity, uploadImages, setProfileImage, deleteImage, addImageFromUrl } from '../services/api';
|
import { getCelebrityById, updateCelebrity, deleteCelebrity, uploadImages, setProfileImage, deleteImage, addImageFromUrl, addAlias, deleteAlias } from '../services/api';
|
||||||
import EditableField from './EditableField';
|
import EditableField from './EditableField';
|
||||||
import './CelebrityProfile.css';
|
import './CelebrityProfile.css';
|
||||||
|
|
||||||
@@ -13,14 +11,17 @@ function CelebrityProfile() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
// Stati per la gestione upload
|
const [selectedFiles, setSelectedFiles] = useState(null);
|
||||||
const [selectedFiles, setSelectedFiles] = useState(null); // Da file a files
|
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [imageUrl, setImageUrl] = useState('');
|
const [imageUrl, setImageUrl] = useState('');
|
||||||
const [isFetchingFromUrl, setIsFetchingFromUrl] = useState(false);
|
const [isFetchingFromUrl, setIsFetchingFromUrl] = useState(false);
|
||||||
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
|
// Stati per la gestione degli alias
|
||||||
|
const [newAlias, setNewAlias] = useState('');
|
||||||
|
const [isAddingAlias, setIsAddingAlias] = useState(false);
|
||||||
|
|
||||||
const fetchProfile = useCallback(() => {
|
const fetchProfile = useCallback(() => {
|
||||||
getCelebrityById(id)
|
getCelebrityById(id)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@@ -43,12 +44,10 @@ function CelebrityProfile() {
|
|||||||
}
|
}
|
||||||
}, [id, navigate, fetchProfile]);
|
}, [id, navigate, fetchProfile]);
|
||||||
|
|
||||||
// Effetto per caricare i file quando `selectedFiles` cambia
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedFiles && selectedFiles.length > 0) {
|
if (selectedFiles && selectedFiles.length > 0) {
|
||||||
handleUpload();
|
handleUpload();
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [selectedFiles]);
|
}, [selectedFiles]);
|
||||||
|
|
||||||
const handleFieldSave = async (fieldName, newValue) => {
|
const handleFieldSave = async (fieldName, newValue) => {
|
||||||
@@ -60,7 +59,7 @@ function CelebrityProfile() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const updatedCelebrity = await updateCelebrity(id, payload);
|
const updatedCelebrity = await updateCelebrity(id, payload);
|
||||||
setCelebrity(prev => ({ ...prev, ...updatedCelebrity })); // Aggiornamento pi├╣ robusto
|
setCelebrity(prev => ({ ...prev, ...updatedCelebrity }));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Errore durante il salvataggio del campo ${fieldName}:`, err);
|
console.error(`Errore durante il salvataggio del campo ${fieldName}:`, err);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -68,10 +67,10 @@ function CelebrityProfile() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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);
|
||||||
alert(`${celebrity.name} è stato eliminato con successo.`);
|
alert(`${celebrity.name} è stato eliminato con successo.`);
|
||||||
navigate('/');
|
navigate('/');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(`Errore durante l'eliminazione: ${err.message}`);
|
setError(`Errore durante l'eliminazione: ${err.message}`);
|
||||||
@@ -79,6 +78,39 @@ function CelebrityProfile() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Gestori per gli Alias ---
|
||||||
|
const handleAddAlias = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newAlias.trim()) return;
|
||||||
|
setIsAddingAlias(true);
|
||||||
|
try {
|
||||||
|
const addedAlias = await addAlias(id, newAlias.trim());
|
||||||
|
setCelebrity(prev => ({
|
||||||
|
...prev,
|
||||||
|
aliases: [...prev.aliases, addedAlias].sort((a, b) => a.alias_name.localeCompare(b.alias_name))
|
||||||
|
}));
|
||||||
|
setNewAlias('');
|
||||||
|
} catch (err) {
|
||||||
|
setError(`Errore durante l'aggiunta dell'alias: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
setIsAddingAlias(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteAlias = async (aliasId) => {
|
||||||
|
if (window.confirm("Sei sicuro di voler eliminare questo alias?")) {
|
||||||
|
try {
|
||||||
|
await deleteAlias(aliasId);
|
||||||
|
setCelebrity(prev => ({
|
||||||
|
...prev,
|
||||||
|
aliases: prev.aliases.filter(a => a.id !== aliasId)
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
setError(`Errore durante l'eliminazione dell'alias: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- Gestori per le Immagini ---
|
// --- Gestori per le Immagini ---
|
||||||
const handleFileChange = (e) => {
|
const handleFileChange = (e) => {
|
||||||
if (e.target.files.length > 0) {
|
if (e.target.files.length > 0) {
|
||||||
@@ -91,7 +123,7 @@ function CelebrityProfile() {
|
|||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await uploadImages(id, selectedFiles); // Chiama la nuova funzione plurale
|
await uploadImages(id, selectedFiles);
|
||||||
fetchProfile();
|
fetchProfile();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(`Upload fallito: ${err.message}`);
|
setError(`Upload fallito: ${err.message}`);
|
||||||
@@ -108,7 +140,7 @@ function CelebrityProfile() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await addImageFromUrl(id, imageUrl);
|
await addImageFromUrl(id, imageUrl);
|
||||||
setImageUrl(''); // Pulisce l'input
|
setImageUrl('');
|
||||||
fetchProfile();
|
fetchProfile();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(`Fetch da URL fallito: ${err.message}`);
|
setError(`Fetch da URL fallito: ${err.message}`);
|
||||||
@@ -120,7 +152,7 @@ function CelebrityProfile() {
|
|||||||
const handleSetProfileImage = async (imageId) => {
|
const handleSetProfileImage = async (imageId) => {
|
||||||
try {
|
try {
|
||||||
const updatedCelebrity = await setProfileImage(id, imageId);
|
const updatedCelebrity = await setProfileImage(id, imageId);
|
||||||
setCelebrity(updatedCelebrity); // Aggiorna lo stato con i dati freschi dal server
|
setCelebrity(updatedCelebrity);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(`Errore nell'impostare l'immagine: ${err.message}`);
|
setError(`Errore nell'impostare l'immagine: ${err.message}`);
|
||||||
}
|
}
|
||||||
@@ -130,17 +162,16 @@ function CelebrityProfile() {
|
|||||||
if (window.confirm("Sei sicuro di voler eliminare questa immagine?")) {
|
if (window.confirm("Sei sicuro di voler eliminare questa immagine?")) {
|
||||||
try {
|
try {
|
||||||
await deleteImage(imageId);
|
await deleteImage(imageId);
|
||||||
fetchProfile(); // Ricarica per aggiornare la galleria
|
fetchProfile();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(`Errore nell'eliminazione dell'immagine: ${err.message}`);
|
setError(`Errore nell'eliminazione dell'immagine: ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Gestori per Drag & Drop ---
|
|
||||||
const handleDragEnter = (e) => { e.preventDefault(); e.stopPropagation(); setIsDraggingOver(true); };
|
const handleDragEnter = (e) => { e.preventDefault(); e.stopPropagation(); setIsDraggingOver(true); };
|
||||||
const handleDragLeave = (e) => { e.preventDefault(); e.stopPropagation(); setIsDraggingOver(false); };
|
const handleDragLeave = (e) => { e.preventDefault(); e.stopPropagation(); setIsDraggingOver(false); };
|
||||||
const handleDragOver = (e) => { e.preventDefault(); e.stopPropagation(); }; // Necessario per permettere il drop
|
const handleDragOver = (e) => { e.preventDefault(); e.stopPropagation(); };
|
||||||
const handleDrop = (e) => {
|
const handleDrop = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -151,10 +182,9 @@ function CelebrityProfile() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
if (loading) return <article aria-busy="true">Caricamento profilo...</article>;
|
if (loading) return <article aria-busy="true">Caricamento profilo...</article>;
|
||||||
if (error && !celebrity) 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 profileImageUrl = celebrity.profile_image
|
const profileImageUrl = celebrity.profile_image
|
||||||
? `/api/uploads/${celebrity.profile_image.file_path}`
|
? `/api/uploads/${celebrity.profile_image.file_path}`
|
||||||
@@ -163,7 +193,7 @@ function CelebrityProfile() {
|
|||||||
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 shoeSystemOptions = [{ value: 'EU', label: 'EU' }, { value: 'US', label: 'US' }, { value: 'UK', label: 'UK' }];
|
const shoeSystemOptions = [{ value: 'EU', label: 'EU' }, { value: 'US', label: 'US' }, { value: 'UK', label: 'UK' }];
|
||||||
const booleanOptions = [{ value: 'true', label: 'Sì' }, { value: 'false', label: 'No' }];
|
const booleanOptions = [{ value: 'true', label: 'Sì' }, { value: 'false', label: 'No' }];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="profile-container">
|
<div className="profile-container">
|
||||||
@@ -183,6 +213,33 @@ function CelebrityProfile() {
|
|||||||
<main className="profile-content">
|
<main className="profile-content">
|
||||||
{error && <p className="error-message"><strong>Errore:</strong> {error}</p>}
|
{error && <p className="error-message"><strong>Errore:</strong> {error}</p>}
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><h3>Alias e Pseudonimi ({celebrity.aliases.length})</h3></summary>
|
||||||
|
<div className="alias-section">
|
||||||
|
<ul className="alias-list">
|
||||||
|
{celebrity.aliases.map(alias => (
|
||||||
|
<li key={alias.id}>
|
||||||
|
<span>{alias.alias_name}</span>
|
||||||
|
<button onClick={() => handleDeleteAlias(alias.id)} className="outline secondary button-icon" title="Elimina alias">×</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{celebrity.aliases.length === 0 && <p><em>Nessun alias registrato.</em></p>}
|
||||||
|
<form onSubmit={handleAddAlias} className="alias-add-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Aggiungi nuovo alias..."
|
||||||
|
value={newAlias}
|
||||||
|
onChange={(e) => setNewAlias(e.target.value)}
|
||||||
|
disabled={isAddingAlias}
|
||||||
|
/>
|
||||||
|
<button type="submit" disabled={!newAlias.trim() || isAddingAlias} aria-busy={isAddingAlias}>
|
||||||
|
Aggiungi
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<details open>
|
<details open>
|
||||||
<summary><h3>Galleria e Upload</h3></summary>
|
<summary><h3>Galleria e Upload</h3></summary>
|
||||||
<div className="gallery-grid">
|
<div className="gallery-grid">
|
||||||
@@ -190,12 +247,8 @@ function CelebrityProfile() {
|
|||||||
<div key={img.id} className="gallery-item">
|
<div key={img.id} className="gallery-item">
|
||||||
<img src={`/api/uploads/${img.file_path}`} alt={`Immagine di ${celebrity.name}`} />
|
<img src={`/api/uploads/${img.file_path}`} alt={`Immagine di ${celebrity.name}`} />
|
||||||
<div className="gallery-item-actions">
|
<div className="gallery-item-actions">
|
||||||
<button onClick={() => handleSetProfileImage(img.id)} disabled={celebrity.profile_image_id === img.id} title="Imposta come immagine profilo">
|
<button onClick={() => handleSetProfileImage(img.id)} disabled={celebrity.profile_image_id === img.id} title="Imposta come immagine profilo">Profilo</button>
|
||||||
Profilo
|
<button onClick={() => handleDeleteImage(img.id)} className="secondary" title="Elimina immagine">X</button>
|
||||||
</button>
|
|
||||||
<button onClick={() => handleDeleteImage(img.id)} className="secondary" title="Elimina immagine">
|
|
||||||
X
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -203,36 +256,16 @@ function CelebrityProfile() {
|
|||||||
<div className="upload-section">
|
<div className="upload-section">
|
||||||
<div>
|
<div>
|
||||||
<h4>Carica da File</h4>
|
<h4>Carica da File</h4>
|
||||||
<div
|
<div className={`drop-zone ${isDraggingOver ? 'dragging-over' : ''}`} onClick={() => fileInputRef.current.click()} onDrop={handleDrop} onDragOver={handleDragOver} onDragEnter={handleDragEnter} onDragLeave={handleDragLeave} aria-busy={isUploading}>
|
||||||
className={`drop-zone ${isDraggingOver ? 'dragging-over' : ''}`}
|
|
||||||
onClick={() => fileInputRef.current.click()}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragEnter={handleDragEnter}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
aria-busy={isUploading}
|
|
||||||
>
|
|
||||||
<input type="file" ref={fileInputRef} onChange={handleFileChange} hidden multiple />
|
<input type="file" ref={fileInputRef} onChange={handleFileChange} hidden multiple />
|
||||||
{isUploading
|
{isUploading ? <p>Caricamento di {selectedFiles.length} file...</p> : <p>Trascina i file qui, o <strong>clicca per selezionare</strong>.</p>}
|
||||||
? <p>Caricamento di {selectedFiles.length} file...</p>
|
|
||||||
: <p>Trascina i file qui, o <strong>clicca per selezionare</strong>.</p>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4>Aggiungi da URL</h4>
|
<h4>Aggiungi da URL</h4>
|
||||||
<div className="grid">
|
<div className="grid">
|
||||||
<input
|
<input type="url" placeholder="https://esempio.com/immagine.jpg" value={imageUrl} onChange={(e) => setImageUrl(e.target.value)} disabled={isFetchingFromUrl} />
|
||||||
type="url"
|
<button onClick={handleFetchFromUrl} disabled={!imageUrl || isFetchingFromUrl} aria-busy={isFetchingFromUrl}>{isFetchingFromUrl ? '...' : 'Aggiungi'}</button>
|
||||||
placeholder="https://esempio.com/immagine.jpg"
|
|
||||||
value={imageUrl}
|
|
||||||
onChange={(e) => setImageUrl(e.target.value)}
|
|
||||||
disabled={isFetchingFromUrl}
|
|
||||||
/>
|
|
||||||
<button onClick={handleFetchFromUrl} disabled={!imageUrl || isFetchingFromUrl} aria-busy={isFetchingFromUrl}>
|
|
||||||
{isFetchingFromUrl ? '...' : 'Aggiungi'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -243,9 +276,9 @@ function CelebrityProfile() {
|
|||||||
<div className="profile-grid">
|
<div className="profile-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} />
|
||||||
<EditableField label="Genere" name="gender" value={celebrity.gender} type="select" options={genderOptions} 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>
|
||||||
<EditableField label="Sito Ufficiale" name="official_website" value={celebrity.official_website} type="url" onSave={handleFieldSave} />
|
<EditableField label="Sito Ufficiale" name="official_website" value={celebrity.official_website} type="url" onSave={handleFieldSave} />
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -3,13 +3,17 @@ const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
|
|||||||
const handleResponse = async (response) => {
|
const handleResponse = async (response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
throw new Error(errorData.detail || 'Si è verificato un errore');
|
throw new Error(errorData.detail || 'Si è verificato un errore');
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCelebrities = async () => {
|
export const getCelebrities = async (searchTerm = '') => {
|
||||||
const response = await fetch(`${API_BASE_URL}/celebrities/`);
|
const params = new URLSearchParams();
|
||||||
|
if (searchTerm) {
|
||||||
|
params.append('search', searchTerm);
|
||||||
|
}
|
||||||
|
const response = await fetch(`${API_BASE_URL}/celebrities/?${params.toString()}`);
|
||||||
return handleResponse(response);
|
return handleResponse(response);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,22 +51,35 @@ export const deleteCelebrity = async (id) => {
|
|||||||
return response.json(); // O un messaggio di successo
|
return response.json(); // O un messaggio di successo
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- NUOVE FUNZIONI PER LE IMMAGINI ---
|
// --- NUOVE FUNZIONI PER GLI ALIAS ---
|
||||||
|
|
||||||
export const uploadImage = async (celebrityId, file) => {
|
export const addAlias = async (celebrityId, aliasName) => {
|
||||||
const formData = new FormData();
|
const response = await fetch(`${API_BASE_URL}/celebrities/${celebrityId}/aliases`, {
|
||||||
formData.append('file', file);
|
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE_URL}/celebrities/${celebrityId}/images`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData, // Il browser imposta automaticamente l'header Content-Type corretto
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ alias_name: aliasName }),
|
||||||
});
|
});
|
||||||
return handleResponse(response);
|
return handleResponse(response);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteAlias = async (aliasId) => {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/celebrities/aliases/${aliasId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
if (response.status === 204) {
|
||||||
|
return { success: true }; // Nessun contenuto da parsare
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.detail || "Errore durante l'eliminazione dell'alias");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- FUNZIONI PER LE IMMAGINI ---
|
||||||
|
|
||||||
export const uploadImages = async (celebrityId, files) => {
|
export const uploadImages = async (celebrityId, files) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
// Aggiunge ogni file allo stesso campo 'files'. Il backend lo interpreterà come una lista.
|
|
||||||
files.forEach(file => {
|
files.forEach(file => {
|
||||||
formData.append('files', file);
|
formData.append('files', file);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user