diff --git a/packages/backend/app/__pycache__/crud.cpython-311.pyc b/packages/backend/app/__pycache__/crud.cpython-311.pyc index 42f3f0d..3c36e02 100644 Binary files a/packages/backend/app/__pycache__/crud.cpython-311.pyc and b/packages/backend/app/__pycache__/crud.cpython-311.pyc differ diff --git a/packages/backend/app/crud.py b/packages/backend/app/crud.py index bac4179..6f1a1ae 100644 --- a/packages/backend/app/crud.py +++ b/packages/backend/app/crud.py @@ -1,4 +1,5 @@ import os +import sqlalchemy as sa from sqlalchemy.orm import Session, joinedload 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 return db.query(models.Celebrity).options( 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() -def get_celebrities(db: Session, skip: int = 0, limit: int = 100): - return db.query(models.Celebrity).offset(skip).limit(limit).all() +def get_celebrities(db: Session, skip: int = 0, limit: int = 100, search: str = None): + 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): db_celebrity = models.Celebrity(**celebrity.model_dump()) @@ -41,6 +56,23 @@ def delete_celebrity(db: Session, celebrity_id: int): db.commit() 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 --- def get_image(db: Session, image_id: int): diff --git a/packages/backend/app/routers/__pycache__/celebrities.cpython-311.pyc b/packages/backend/app/routers/__pycache__/celebrities.cpython-311.pyc index 878e1dc..547d82b 100644 Binary files a/packages/backend/app/routers/__pycache__/celebrities.cpython-311.pyc and b/packages/backend/app/routers/__pycache__/celebrities.cpython-311.pyc differ diff --git a/packages/backend/app/routers/celebrities.py b/packages/backend/app/routers/celebrities.py index d3a4807..1253790 100644 --- a/packages/backend/app/routers/celebrities.py +++ b/packages/backend/app/routers/celebrities.py @@ -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 typing import List +from typing import List, Optional import uuid import shutil +import httpx from .. import crud, models, schemas from ..database import get_db @@ -15,12 +16,12 @@ router = APIRouter( @router.post("/", response_model=schemas.Celebrity, status_code=201) 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) @router.get("/", response_model=List[schemas.Celebrity]) -def read_celebrities(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): - celebrities = crud.get_celebrities(db, skip=skip, limit=limit) +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, search=search) return celebrities @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") 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) 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) 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}") 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) try: 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.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/'): 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)) if content_length > 10 * 1024 * 1024: 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.raise_for_status() - # Determina l'estensione del file file_extension = content_type.split('/')[-1] 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.") 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) def delete_image(image_id: int, db: Session = Depends(get_db)): db_image = crud.delete_image(db, image_id=image_id) diff --git a/packages/frontend/src/components/CelebrityList.jsx b/packages/frontend/src/components/CelebrityList.jsx index b01acfe..9a38847 100644 --- a/packages/frontend/src/components/CelebrityList.jsx +++ b/packages/frontend/src/components/CelebrityList.jsx @@ -1,36 +1,43 @@ -// packages/frontend/src/components/CelebrityList.jsx - import React, { useState, useEffect, useMemo } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { getCelebrities, deleteCelebrity } from '../services/api'; -import './CelebrityList.css'; // Importa il nuovo CSS +import './CelebrityList.css'; -// Icone SVG semplici per le azioni const EditIcon = () => ; const DeleteIcon = () => ; const MoreIcon = () => ; - function CelebrityList() { const [celebrities, setCelebrities] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [searchTerm, setSearchTerm] = useState(''); - - // Stato per la paginazione + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); 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(() => { fetchCelebrities(); - }, []); + }, [debouncedSearchTerm]); const fetchCelebrities = async () => { try { setLoading(true); - const data = await getCelebrities(); + const data = await getCelebrities(debouncedSearchTerm); setCelebrities(data); setError(null); } catch (err) { @@ -51,24 +58,18 @@ function CelebrityList() { } }; - // Memoizzazione dei dati filtrati e paginati per performance - const filteredCelebrities = useMemo(() => { - return celebrities.filter(c => - c.name.toLowerCase().includes(searchTerm.toLowerCase()) - ); - }, [celebrities, searchTerm]); - + // La paginazione ora opera sulla lista (già filtrata) ricevuta dal backend const paginatedCelebrities = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; - return filteredCelebrities.slice(startIndex, startIndex + itemsPerPage); - }, [filteredCelebrities, currentPage, itemsPerPage]); + return celebrities.slice(startIndex, startIndex + itemsPerPage); + }, [celebrities, currentPage, itemsPerPage]); - const totalPages = Math.ceil(filteredCelebrities.length / itemsPerPage); + const totalPages = Math.ceil(celebrities.length / itemsPerPage); const renderPagination = () => (