11"use client" ;
22
33import { useEffect , useState } from "react" ;
4+ import { createPortal } from "react-dom" ;
45import { Box } from "@/components/shared/dashboard" ;
56import { Button } from "@/components/ui/button" ;
67import { Position , Project , SkillCategory } from "@/types/home/codev" ;
@@ -37,8 +38,11 @@ export default function FilterCodevs({
3738 const [ showFilter , setShowFilter ] = useState ( false ) ;
3839 const [ positions , setPositions ] = useState < Position [ ] > ( [ ] ) ;
3940 const [ projects , setProjects ] = useState < Project [ ] > ( [ ] ) ;
40-
4141 const [ supabase , setSupabase ] = useState < any > ( null ) ;
42+
43+ // Track button position for dropdown placement
44+ const [ buttonRef , setButtonRef ] = useState < HTMLDivElement | null > ( null ) ;
45+ const [ dropdownPosition , setDropdownPosition ] = useState ( { top : 0 , right : 0 } ) ;
4246
4347 useEffect ( ( ) => {
4448 const supabaseClient = createClientClientComponent ( ) ;
@@ -69,6 +73,17 @@ export default function FilterCodevs({
6973 fetchData ( ) ;
7074 } , [ supabase ] ) ;
7175
76+ // Calculate dropdown position when button ref changes or filter opens
77+ useEffect ( ( ) => {
78+ if ( buttonRef && showFilter ) {
79+ const rect = buttonRef . getBoundingClientRect ( ) ;
80+ setDropdownPosition ( {
81+ top : rect . bottom + 8 , // 8px gap below button
82+ right : window . innerWidth - rect . right , // Align right edge
83+ } ) ;
84+ }
85+ } , [ buttonRef , showFilter ] ) ;
86+
7287 const toggleFilter = ( ) => setShowFilter ( ( prev ) => ! prev ) ;
7388
7489 const clearFilter = ( ) =>
@@ -88,106 +103,153 @@ export default function FilterCodevs({
88103 } ) ;
89104 } ;
90105
106+ // Render dropdown using portal to escape stacking context
107+ const renderDropdown = ( ) => {
108+ if ( typeof window === "undefined" || ! showFilter ) return null ;
109+
110+ return createPortal (
111+ < >
112+ { /* Backdrop overlay - close on click outside */ }
113+ < div
114+ className = "fixed inset-0 z-40"
115+ onClick = { ( ) => setShowFilter ( false ) }
116+ aria-hidden = "true"
117+ />
118+
119+ { /* Filter dropdown panel */ }
120+ < div
121+ className = "fixed z-50 max-h-[calc(100vh-6rem)] min-h-[300px] w-[90vw] overflow-auto
122+ sm:w-96 md:w-80 rounded-2xl bg-white/95 backdrop-blur-md p-6 shadow-2xl
123+ dark:bg-gray-900/95 border border-gray-200 dark:border-gray-700"
124+ style = { {
125+ top : `${ dropdownPosition . top } px` ,
126+ right : `${ dropdownPosition . right } px` ,
127+ } }
128+ >
129+ { /* Header with clear button */ }
130+ < div className = "mb-4 flex items-center justify-between" >
131+ < h3 className = "font-semibold text-gray-900 dark:text-white drop-shadow-sm" >
132+ Filters
133+ </ h3 >
134+ < button
135+ onClick = { clearFilter }
136+ className = "text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400
137+ dark:hover:text-blue-300 transition-colors duration-200 px-2 py-1
138+ rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
139+ >
140+ Clear all
141+ </ button >
142+ </ div >
143+
144+ { /* Filter tabs */ }
145+ < Tabs defaultValue = "position" className = "w-full" >
146+ < TabsList className = "grid w-full grid-cols-3 bg-gray-100 dark:bg-gray-800
147+ border border-gray-200 dark:border-gray-700" >
148+ < TabsTrigger value = "position" > Position</ TabsTrigger >
149+ < TabsTrigger value = "status" > Status</ TabsTrigger >
150+ < TabsTrigger value = "projects" > Projects</ TabsTrigger >
151+ </ TabsList >
152+
153+ { /* Position filter tab */ }
154+ < TabsContent value = "position" className = "mt-4 space-y-2" >
155+ { positions . map ( ( pos ) => (
156+ < div
157+ key = { pos . id }
158+ className = "flex items-center space-x-2 rounded-lg p-2 transition-colors
159+ hover:bg-gray-100 dark:hover:bg-gray-800"
160+ >
161+ < Checkbox
162+ id = { `pos-${ pos . id } ` }
163+ checked = { filters . positions . includes ( pos . name || "" ) }
164+ onCheckedChange = { ( ) =>
165+ handleCheckedChange ( "positions" , pos . name || "" )
166+ }
167+ />
168+ < label
169+ htmlFor = { `pos-${ pos . id } ` }
170+ className = "text-sm cursor-pointer text-gray-900 dark:text-gray-100"
171+ >
172+ { pos . name }
173+ </ label >
174+ </ div >
175+ ) ) }
176+ </ TabsContent >
177+
178+ { /* Status filter tab */ }
179+ < TabsContent value = "status" className = "mt-4 space-y-2" >
180+ { AVAILABILITY_STATUS . map ( ( status ) => (
181+ < div
182+ key = { status }
183+ className = "flex items-center space-x-2 rounded-lg p-2 transition-colors
184+ hover:bg-gray-100 dark:hover:bg-gray-800"
185+ >
186+ < Checkbox
187+ id = { `status-${ status } ` }
188+ checked = { filters . availability . includes ( status ) }
189+ onCheckedChange = { ( ) =>
190+ handleCheckedChange ( "availability" , status )
191+ }
192+ />
193+ < label
194+ htmlFor = { `status-${ status } ` }
195+ className = "text-sm cursor-pointer text-gray-900 dark:text-gray-100"
196+ >
197+ { status }
198+ </ label >
199+ </ div >
200+ ) ) }
201+ </ TabsContent >
202+
203+ { /* Projects filter tab */ }
204+ < TabsContent value = "projects" className = "mt-4 space-y-2" >
205+ { projects . map ( ( project ) => (
206+ < div
207+ key = { project . id }
208+ className = "flex items-center space-x-2 rounded-lg p-2 transition-colors
209+ hover:bg-gray-100 dark:hover:bg-gray-800"
210+ >
211+ < Checkbox
212+ id = { project . id }
213+ checked = { filters . projects . includes ( project . id ) }
214+ onCheckedChange = { ( ) =>
215+ handleCheckedChange ( "projects" , project . id )
216+ }
217+ />
218+ < label
219+ htmlFor = { project . name }
220+ className = "text-sm cursor-pointer text-gray-900 dark:text-gray-100"
221+ >
222+ { project . name }
223+ </ label >
224+ </ div >
225+ ) ) }
226+ </ TabsContent >
227+ </ Tabs >
228+ </ div >
229+ </ > ,
230+ document . body
231+ ) ;
232+ } ;
233+
91234 return (
92- < div className = "relative flex justify-end" >
235+ < div className = "relative flex justify-end" ref = { setButtonRef } >
236+ { /* Filter toggle button */ }
93237 < Button
94238 variant = "default"
95239 size = "sm"
96240 onClick = { toggleFilter }
97241 className = { cn (
98242 "text-white transition-all duration-300 backdrop-blur-sm border shadow-lg" ,
99243 showFilter
100- ? "bg-gradient-to-r from-pink-500/80 to-rose-600/80 hover:from-pink-600 hover:to-rose-700 border-pink-400/50"
101- : "bg-gradient-to-r from-customBlue-500/80 to-indigo-500/80 hover:from-customBlue-600 hover:to-indigo-600 border-customBlue-400/50"
244+ ? "bg-gradient-to-r from-pink-500/80 to-rose-600/80 hover:from-pink-600 hover:to-rose-700 border-pink-400/50"
245+ : "bg-gradient-to-r from-customBlue-500/80 to-indigo-500/80 hover:from-customBlue-600 hover:to-indigo-600 border-customBlue-400/50"
102246 ) }
103247 >
104248 { showFilter ? "Close filter" : "Filter" }
105249 </ Button >
106250
107- < div
108- className = { `absolute right-0 top-12 z-[1] max-h-[calc(100vh-20rem)] min-h-[300px] w-[300%] overflow-auto
109- sm:-right-3/4 sm:w-96 md:right-0 md:w-80 rounded-2xl bg-white/10 backdrop-blur-md p-6 shadow-2xl dark:bg-white/5 border border-white/20 dark:border-white/10
110- ${ ! showFilter && "hidden" }
111- ` }
112- >
113- < div className = "mb-4 flex items-center justify-between" >
114- < h3 className = "font-semibold text-white drop-shadow-sm" > Filters</ h3 >
115- < button
116- onClick = { clearFilter }
117- className = "text-sm text-customBlue-300 hover:text-customBlue-100 dark:text-customBlue-400 dark:hover:text-customBlue-200 transition-colors duration-200 px-2 py-1 rounded-md hover:bg-white/20 dark:hover:bg-white/10"
118- >
119- Clear all
120- </ button >
121- </ div >
122-
123- < Tabs defaultValue = "position" className = "w-full" >
124- < TabsList className = "grid w-full grid-cols-3 bg-white/10 backdrop-blur-sm border border-white/20 dark:border-white/10" >
125- < TabsTrigger value = "position" > Position</ TabsTrigger >
126- < TabsTrigger value = "status" > Status</ TabsTrigger >
127- < TabsTrigger value = "projects" > Projects</ TabsTrigger >
128- </ TabsList >
129-
130- < TabsContent value = "position" className = "mt-4 space-y-2" >
131- { positions . map ( ( pos ) => (
132- < div
133- key = { pos . id }
134- className = "flex items-center space-x-2 rounded-lg p-2 transition-colors hover:bg-white/20 dark:hover:bg-white/10"
135- >
136- < Checkbox
137- id = { `pos-${ pos . id } ` }
138- checked = { filters . positions . includes ( pos . name || "" ) }
139- onCheckedChange = { ( ) =>
140- handleCheckedChange ( "positions" , pos . name || "" )
141- }
142- />
143- < label htmlFor = { `pos-${ pos . id } ` } className = "text-sm cursor-pointer text-white dark:text-gray-200" >
144- { pos . name }
145- </ label >
146- </ div >
147- ) ) }
148- </ TabsContent >
149-
150- < TabsContent value = "status" className = "mt-4 space-y-2" >
151- { AVAILABILITY_STATUS . map ( ( status ) => (
152- < div
153- key = { status }
154- className = "flex items-center space-x-2 rounded-lg p-2 transition-colors hover:bg-white/20 dark:hover:bg-white/10"
155- >
156- < Checkbox
157- id = { `status-${ status } ` }
158- checked = { filters . availability . includes ( status ) }
159- onCheckedChange = { ( ) =>
160- handleCheckedChange ( "availability" , status )
161- }
162- />
163- < label htmlFor = { `status-${ status } ` } className = "text-sm cursor-pointer text-white dark:text-gray-200" >
164- { status }
165- </ label >
166- </ div >
167- ) ) }
168- </ TabsContent >
169-
170- < TabsContent value = "projects" className = "mt-4 space-y-2" >
171- { projects . map ( ( project ) => (
172- < div
173- key = { project . id }
174- className = "flex items-center space-x-2 rounded-lg p-2 transition-colors hover:bg-white/20 dark:hover:bg-white/10"
175- >
176- < Checkbox
177- id = { project . id }
178- checked = { filters . projects . includes ( project . id ) }
179- onCheckedChange = { ( ) =>
180- handleCheckedChange ( "projects" , project . id )
181- }
182- />
183- < label htmlFor = { project . name } className = "text-sm cursor-pointer text-white dark:text-gray-200" >
184- { project . name }
185- </ label >
186- </ div >
187- ) ) }
188- </ TabsContent >
189- </ Tabs >
190- </ div >
251+ { /* Render dropdown in portal (z-50 above everything) */ }
252+ { renderDropdown ( ) }
191253 </ div >
192254 ) ;
193- }
255+ }
0 commit comments