@@ -29,13 +29,20 @@ import { Skeleton } from "@/components/ui/skeleton";
2929import { FilterPanel } from "@/components/filters/filter-panel" ;
3030import { useFilteredProfiles } from "@/hooks/use-filtered-profiles" ;
3131import { useFilterStore } from "@/store/filter-store" ;
32- import { ROLE_TYPE_LABELS , EXPERIENCE_LABELS , CROSS_DOMAIN_TECHS } from "@/lib/constants" ;
32+ import { ROLE_TYPE_LABELS , EXPERIENCE_LABELS } from "@/lib/constants" ;
3333import { cn } from "@/lib/utils" ;
34- import { useState } from "react" ;
34+ import React , { useState , useEffect } from "react" ;
3535
36- // Suggestions de compétences couvrant tous les métiers de l'informatique
37- // (dev, systèmes, cybersécurité, support, data, design…), pas uniquement le dev web.
38- const QUICK_FILTERS = CROSS_DOMAIN_TECHS ;
36+ const QUICK_FILTERS = [
37+ "React" ,
38+ "JavaScript" ,
39+ "Python" ,
40+ "Flutter" ,
41+ "Node.js" ,
42+ "TypeScript" ,
43+ "PHP" ,
44+ "Vue.js" ,
45+ ] ;
3946
4047function formatLastSeen ( lastSeenAt ?: string ) : string | null {
4148 if ( ! lastSeenAt ) return null ;
@@ -47,18 +54,15 @@ function formatLastSeen(lastSeenAt?: string): string | null {
4754 return null ;
4855}
4956
50- function ProfileCard ( { profile } : { profile : Profile } ) {
57+ const ProfileCard = React . memo ( function ProfileCard ( { profile } : { profile : Profile } ) {
5158 const lastSeen = formatLastSeen ( profile . last_seen_at ) ;
5259
5360 return (
5461 < Link to = { `/contributeurs/${ profile . username } ` } >
5562 < article className = "glass-panel group relative rounded-xl border border-white/10 p-5 transition-all hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-[0_8px_24px_rgba(78,222,163,0.08)] active:scale-[0.99] cursor-pointer" >
56- { /* Ambient glow */ }
5763 < div className = "pointer-events-none absolute right-0 top-0 h-20 w-20 rounded-bl-full bg-primary/6 blur-xl transition-colors group-hover:bg-primary/12" />
5864
59- { /* Header */ }
6065 < div className = "mb-4 flex items-start justify-between gap-3" >
61- { /* Avatar + badge disponible */ }
6266 < div className = "relative shrink-0" >
6367 < Avatar className = "h-14 w-14 border-2 border-white/15" >
6468 < AvatarImage src = { profile . avatar_url } alt = { profile . full_name } />
@@ -76,7 +80,6 @@ function ProfileCard({ profile }: { profile: Profile }) {
7680 ) }
7781 </ div >
7882
79- { /* Nom + rôle + ville */ }
8083 < div className = "flex-1 min-w-0" >
8184 < h3 className = "truncate text-sm font-bold leading-tight text-foreground group-hover:text-primary transition-colors" >
8285 { profile . full_name }
@@ -88,15 +91,13 @@ function ProfileCard({ profile }: { profile: Profile }) {
8891 < MapPin className = "h-3 w-3 shrink-0" />
8992 < span className = "truncate" > { profile . city || "Congo" } </ span >
9093 </ div >
91- { /* Badge Disponible visible en texte */ }
9294 { profile . open_to_collaboration && (
9395 < span className = "mt-1.5 inline-flex items-center gap-1 rounded-full bg-primary/10 border border-primary/25 px-2 py-0.5 text-[11px] font-medium text-primary" >
9496 Disponible
9597 </ span >
9698 ) }
9799 </ div >
98100
99- { /* Niveau d'expérience */ }
100101 < Badge
101102 variant = "outline"
102103 className = "shrink-0 border-white/10 bg-white/5 text-xs text-muted-foreground"
@@ -105,7 +106,6 @@ function ProfileCard({ profile }: { profile: Profile }) {
105106 </ Badge >
106107 </ div >
107108
108- { /* Techs + GitHub + activité */ }
109109 < div className = "flex flex-wrap items-center gap-1.5 border-t border-white/10 pt-3" >
110110 { profile . tech_stack . slice ( 0 , 3 ) . map ( ( tech ) => (
111111 < span
@@ -143,13 +143,27 @@ function ProfileCard({ profile }: { profile: Profile }) {
143143 </ article >
144144 </ Link >
145145 ) ;
146- }
146+ } ) ;
147147
148148export function ContributorsPage ( ) {
149149 const { profiles, isLoading, total, page, totalPages, setPage } =
150150 useFilteredProfiles ( { pageSize : 18 } ) ;
151151 const { techStack, searchQuery, setTechStack, setSearchQuery } = useFilterStore ( ) ;
152152 const [ mobileFilterOpen , setMobileFilterOpen ] = useState ( false ) ;
153+ const [ localSearch , setLocalSearch ] = useState ( searchQuery ) ;
154+
155+ useEffect ( ( ) => {
156+ setLocalSearch ( searchQuery ) ;
157+ } , [ searchQuery ] ) ;
158+
159+ useEffect ( ( ) => {
160+ const t = setTimeout ( ( ) => {
161+ if ( localSearch !== searchQuery ) {
162+ setSearchQuery ( localSearch ) ;
163+ }
164+ } , 300 ) ;
165+ return ( ) => clearTimeout ( t ) ;
166+ } , [ localSearch , searchQuery , setSearchQuery ] ) ;
153167
154168 if ( isLoading && page === 1 ) {
155169 return (
@@ -166,7 +180,6 @@ export function ContributorsPage() {
166180
167181 return (
168182 < div className = "mx-auto max-w-7xl px-4 pb-24 pt-6 md:pb-8" >
169- { /* Page header */ }
170183 < div className = "mb-5 flex items-end justify-between" >
171184 < div >
172185 < h1 className = "text-2xl font-bold tracking-tight text-foreground md:text-3xl" >
@@ -187,18 +200,16 @@ export function ContributorsPage() {
187200 </ Button >
188201 </ div >
189202
190- { /* Search bar */ }
191203 < div className = "relative mb-4" >
192204 < Search className = "absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
193205 < Input
194- placeholder = "Rechercher par nom, competence ..."
206+ placeholder = "Rechercher par nom, compétence ..."
195207 value = { searchQuery }
196208 onChange = { ( e ) => setSearchQuery ( e . target . value ) }
197209 className = "bg-white/5 border-white/10 pl-10 text-sm focus:border-primary focus:ring-1 focus:ring-primary/30"
198210 />
199211 </ div >
200212
201- { /* Quick filter chips */ }
202213 < div className = "mb-6 flex flex-wrap gap-2" >
203214 { QUICK_FILTERS . map ( ( tech ) => {
204215 const isActive = techStack . includes ( tech ) ;
@@ -225,7 +236,6 @@ export function ContributorsPage() {
225236 } ) }
226237 </ div >
227238
228- { /* Sidebar + grid */ }
229239 < div className = "grid gap-5 lg:grid-cols-[280px_1fr]" >
230240 < aside className = "hidden lg:block" >
231241 < div className = "sticky top-20" >
@@ -234,7 +244,6 @@ export function ContributorsPage() {
234244 </ aside >
235245
236246 < div >
237- { /* Talent grid — large cards on desktop, list-style on mobile */ }
238247 < div className = "grid gap-4 sm:grid-cols-2 xl:grid-cols-3" >
239248 { profiles . map ( ( profile ) => (
240249 < ProfileCard key = { profile . id } profile = { profile } />
@@ -243,13 +252,12 @@ export function ContributorsPage() {
243252 { profiles . length === 0 && ! isLoading && (
244253 < div className = "col-span-full py-20 text-center" >
245254 < Handshake className = "mx-auto mb-4 h-12 w-12 text-muted-foreground/30" />
246- < p className = "font-semibold text-muted-foreground" > Aucun contributeur trouve </ p >
255+ < p className = "font-semibold text-muted-foreground" > Aucun contributeur trouvé </ p >
247256 < p className = "mt-1 text-sm text-muted-foreground/60" > Essayez de modifier vos filtres</ p >
248257 </ div >
249258 ) }
250259 </ div >
251260
252- { /* Pagination */ }
253261 { totalPages > 1 && (
254262 < div className = "mt-8 flex items-center justify-center gap-3" >
255263 < Button
@@ -260,7 +268,7 @@ export function ContributorsPage() {
260268 className = "gap-1 border border-white/10 bg-white/5 hover:bg-white/8 disabled:opacity-30"
261269 >
262270 < ChevronLeft className = "h-4 w-4" />
263- Precedent
271+ Précédent
264272 </ Button >
265273 < span className = "text-xs text-muted-foreground" >
266274 { page } / { totalPages }
@@ -280,7 +288,6 @@ export function ContributorsPage() {
280288 </ div >
281289 </ div >
282290
283- { /* Mobile filter drawer */ }
284291 { mobileFilterOpen && (
285292 < div className = "fixed inset-0 z-[600] flex flex-col lg:hidden" >
286293 < div className = "flex-1" onClick = { ( ) => setMobileFilterOpen ( false ) } />
@@ -313,7 +320,7 @@ export function ContributorsPage() {
313320 className = "w-full bg-primary text-primary-foreground hover:bg-primary/90"
314321 onClick = { ( ) => setMobileFilterOpen ( false ) }
315322 >
316- Voir { profiles . length } resultat { profiles . length !== 1 ? "s" : "" }
323+ Voir { profiles . length } résultat { profiles . length !== 1 ? "s" : "" }
317324 </ Button >
318325 </ div >
319326 </ div >
0 commit comments