From 8ed8fcde6c8c21bfd7f23a063a97502aa6934503 Mon Sep 17 00:00:00 2001 From: Nicola Malizia Date: Fri, 10 Oct 2025 21:05:58 +0200 Subject: [PATCH] aliases --- .../app/__pycache__/crud.cpython-311.pyc | Bin 5686 -> 7670 bytes packages/backend/app/crud.py | 38 ++++- .../__pycache__/celebrities.cpython-311.pyc | Bin 9684 -> 11081 bytes packages/backend/app/routers/celebrities.py | 38 +++-- .../frontend/src/components/CelebrityList.jsx | 53 +++---- .../src/components/CelebrityProfile.css | 47 +++++- .../src/components/CelebrityProfile.jsx | 135 +++++++++++------- packages/frontend/src/services/api.js | 39 +++-- 8 files changed, 239 insertions(+), 111 deletions(-) diff --git a/packages/backend/app/__pycache__/crud.cpython-311.pyc b/packages/backend/app/__pycache__/crud.cpython-311.pyc index 42f3f0dd3b7a450ee14b916382ea834e74259e5c..3c36e026fefc6ce0871fe6292b6aa4b1a06c6092 100644 GIT binary patch delta 2677 zcmaJ@TWl298J;t@o!!~pnZ1nHfB_TR(6!BCyTaufE{2oJl#SB;jUl{HYp4F zU?zDHXt69+fn@gePS?^tPOj*j=0xcEWZ99^KI= zXEi(ewvvx>LCn#nVlCUSC=QF_txaqLq()n++9pWNmK3xTKqp$XodoUVD(w_VQ(T(n z4p~5_ZRIq0*ufbYU|8+to#2~lZvb9Ln}>R;y%BgB&I{IU0=m;C=Y;HLW|E(5KL3QsT|NI=08jGD$iBNqEAVu@2>Hmq+Y5q4JZRX!o(S)Kh^ zJg9~WokE(Odnr#3%)c>Cw%#Oq2l;Tsa(3|`0+8P>|Ca$ z>YGO}Fn)x`XM(F}cFC=lt{35Urza;}r+SEP#GRx0sZpzAQltg zL&ZfWyZNAy7_v2+1qS--{u^LUSdPYTpO`=K;ZHrQuO8j*MYk_SbB$>3MLG=EwGmGn zc_N81NC$wq<4sl2;!QzQtmTfLd)x0+@bixTm1*5IJ}?$kuY1Vr-cvXCdgk6G^KipF z{5Tqa@0G`9hc|0>duHE~InXc%K0Z}9_ju+W*zS+|^FX=rEy{C4(uJ?O- z|B{|-=()N+c%b$ZooH-)G1V*iDD}dbOhL2*n(=4%)?0N zRWgc|LU*FFQmQ%?*AGE33e_s5yHGL&kPQX#z(z;Ek1)*M)v_xlZr-rhn%EB-eeCbr zZ%KwZ;e{Wf1z)wn1qd2$nw3`VHw+9HD+%IA1E&MkRZhPQ;t_T>a{8qVy!i^xqhh-= z01xw%m*bz<&gj75c!&Yd4l`mqJ2z6$irN$V}LX1QD7c~$Ndq&obcGNJfnAU@XOua zsf&+{OLgOtXIx^}Vtf1ev9zE(N_MgBEv&T8+M0NbA>#_oT zip@yeeq;WP`>NNuvu@i=J_D$tX08Lft5O zMse=MvYu)RL`g3jsduW&vdZRD+eDgSzmGJ3u-56U7{uTew2Rl&N}BK$r#exp6s|b* zRbY>^zv@RJ|Fr1W$4>&CRf5=_fcO6wR@CKT;`%cW>{iA~jSR2&Z4fvgQpfYXWEITh#2T1&%;A-G| zQzC>c3*kBbSr#&nT3>c2J@PY^?7MTNDZupcIXHfxpK30j7K7=_9!{-? vZnn{K#}$&k_lu?g(_)cNf7#(3{W*VI4{sh^JJTed;eP-5-{*hKUoU+( zviwyn7E$25H@kiLJXMrmkm(((d)fIFE}tt*VWgo{k*W>SRI6%Jt7=4H8q*tk)2JFv zvuXlwI4Uz8Ye8q$ie3$|J}C)17K?z?WOT)-hNWyd>X=LN0hkILV&Fh~0azTg(e5J@ za7=(U*3*J@68N~}Q!-r|_(V_7GENeFvLH)6k)fT*av)2iEX@K(8w43f=9E&uI77}+ z6OwB1?YvIJ4gIz_rcQ`aGp^pT#5d%yx)c&`C7%)h^yS3GSlpUZ?vgi45mDAA#RuA{ zNDe#=_}r%WL7R&HVQ5kFPX*v_Qxx@sB}+3Vc0jLeGXP(A->**Vdrs5!EvTsGd7KZSW*DGk1yzMn@Ek%>{7XyS6j$49 ze~MACu*He!XSxkH@pH-+o#?Gon8-I7RPQ-5cCT47Y!S5ko0SYq_q@FfHbdh3*jZwW zpHiLi6!1KakO3&E!7E>cyQ>29x*o=^*PFFPhmV2y1@SojDnyvfJenCtKZE18@uLXO zBJ38f?+PC%+1Hm^^(%Z_oEaF-A3;4nsbuc9O}+t!dvl=BFZUzFLo3lt8^+<<=Fr2)4OO>ryliZ>2?DZBk2v(QQ4 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 878e1dcb93cf268e419057b2b1d6de4be68884c6..547d82b1b3f4bd9eae598cf39dbba3b92881a0df 100644 GIT binary patch delta 4117 zcmb7HeQZ2JW-?3AA*a&nU{z3m?M_?>t zh#PoL;v^w$%$VXP#*68ij5%)3SmGAe*QIOYwVwOO1f3IoVW&5de(lwI!D^2A__B%MO;@Vu%LGUxnBL zh@ncvFZ3!6Emeq-l2K@&NJeRh#M;qV^xToO7R6BH!~jrvA?>Gwv;20afcTkqp!Q3L(AuBTl!9g&q>_N*dDdDZx;tSG7M*{GoE4yKg6YI-G?hki1Bb4ah_ zJDb3c!12LF1yTo%YM&r-GA}14#3ZoPp2}eylPP7B$+rfp@WgqV*o~k?_n>s8=7Xt8E%H35pM8TLFsx~@se%^7bF~t ziX;Tuq!mdRNXbFlX*pl^E+s2beT5=nYjS{zq6ZShhcpAH+F@yuW=r2U8aKCt(2aU6 zK<2o6PIu86xk46ni+h$L1?R}JbEN1TnLDszaTj!M5~RO3{$NW7=n^N8&s{^M7zNK} z^TEmN`LohBf%srdHAr$knM%Vd>gf9>pMhC5(Vv@qKIV$`u`3v4(hWw9^i$JrK1f3~ zm-r5PrN-^VX$Te)*#v}vE!FrpGB7PEIJO0df)OmQl=Pq+Nr*bkLx4Hhe112F&`2MOkwmH4vU_I`xM@%9V!1vNDd|!ynNB4YSyAJ$GXI9;n^_*ZX?gP!Tl~ys;VN~5oNz(Ka`E0r9E|8soj2Tn*bMzD2);)JD zwxT66A6o2RI$yAiE?Y*6meGQ4lmtO(+Eofq>>`^RBX<8!FmjF8ip9jl^i!jk&qI)6 z^hf3ndf8b!E9q|-)*p=!3ge+DPX@r81!LMah_QcMfvi+WKaTGLvKA55U72rfU0T*n zs3>Zn7*N$Djl=jL0Iw?swRR1KIZVC;s@vgHkb(VUa}{kH=DQb71>4}VZLnw?q&tI6 z4R!r!{2IGD~+@zh=OSB{BLd>r5L< z+7R~HI3`WCYB`n?sZ8>;tdOtLW3~j}Oh2;u&8I*C-BbQZ|7IJoIrJ0!Du?tv-Kt=x zo%a8@gc_7Rrgv%`bigrW)}!drqerVIdfD+4{@Zl3^P6=Rz*>8z40O=tzIo6253za$ zpSgGO2Wgup750ES*$YI~P37d%WElH#fFpiL#Xr$ zlEX+w=(&da*eDv+9+A(TmzDe;Lb8M$15wnj)pHUx%b_7t^p_38+u|rZj^qTA*MO*^ z{PhWv08X(3!K35Jhcwfex4}FELx`w+myUQ3?h5F~_&ctKqAR@YiWFUuRj#2X4YB** zbUv~|?|9!4@A-pt(l;R9ZE2_1eA~sl{wV#-_t)7iPltHGp#S+C6FiR-&(_&dIX$NR zIc9zIuWfXuX?E5-Jiz_V_vPN(}>(*iR^SORzIi)w76@QwXF*XOyJZwlJBY$6K0aG0tr1*En`O zvR^EpoBA_Z>3muqAm4#;9zMkm*V?Mc^ZW`oQuu$~6)sd*|E_Q?g|#wnu{u!R>|=lsijZ;|g^>FSw3y|8Wa zTJ}a3&Dt12hk?H`{8+>|gzN(=L~NtcaR06*-nYsDnQvvyg0#4!*t?hUusGH}CTM;F zJUYHApj|)xad^W}m>+|MA;tJ$yXYxyKdiBZqpxYqlN4bR0CRFxV8#cjy={W8rTMlK Jx**iWe*u<*$YTHi delta 3014 zcma)8Z){Ul6u+;1um85LUAwhg*LGdkb?f*uCx`;uWE)T>2*?CUoiy9t+bq7W9Z_`3a#FqQQYvIO+(>7TD+=f;^K=kgB_}%}`PMAw5oFIihbUJSaCm^D zYJPzW9PJP0z(soVtC^3=Tfpc8M*nig|BT94z)=kxJnfJuKTlMEImF0w)k_oqz`W`B zdIK;^s>}yV=A*4C@Klrc1UU@C`U1GY|FZsC7!T$t=NUrsuM z_EKUvBdLjGBF%2pxP-MVVDz#vOXU~@quU8I0kESr0C``wK_$K5D4}Q(@xBe^k=ejn z69EEnQ6C5yJH#h1o<%XMSddyZz!7rO;hJ`Y$LV=-V#}1HgFPyVo)Y%(Q zyC@Jg;s_e}cGR)sj+YI1PUuQj>3qG8$MqP#^KpzTQ<6JDay*gj9aJ>Bm0fbi1t0U2 z`7C?Dpc!uMTh?CIW-F8Wg)BjQPt59^?8CCZ%Ox}F?&BR3u{zi%<(-xyRL;%KWlPv^ z3V#B z9AN`0b+7c%jm+o`3$1LmHgrCp$k*mEbS8z(O0i}oxeSneLr%R_jbyVJrZ17ioPw~6FRVDq|Yv;L~9YHO=>wYeDORxv7=uL*jPuZaT6 zQYFfM2TZuuY>93(UfZZgxlKU%Nt7o=TbETo=`7n;ub-^9qP(`O%cQ?o=33jVFOnc zXrS25G7?`6#cq?yt!yt<=j)LUv0v-FW0)fz_8CGkW?oJAOfAWjJfbRX^eE`?U!S%@ zBqXaB1Yw3mrylM*LxNLFzca)=wdCt`&yb=c{4+zWNBD=G3x6NF9K9T!+^Ew1p(b(A8Anb?oM?`*nPPnJn@6L%+`(DTiixGL^z;ht0 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 = () => (
- Pagina {currentPage} di {totalPages} + Pagina {currentPage} di {totalPages} ({celebrities.length} risultati)