@@ -10,7 +10,7 @@ import {
1010 DialogTitle ,
1111} from "@/components/ui/dialog" ;
1212import { Codev , InternalStatus } from "@/types/home/codev" ;
13- import { Search , Users } from "lucide-react" ;
13+ import { Search , Users , X } from "lucide-react" ;
1414import toast from "react-hot-toast" ;
1515import { useModal } from "@/hooks/use-modal-users" ;
1616import { createClientClientComponent } from "@/utils/supabase/client" ;
@@ -43,51 +43,71 @@ const calculateYearsFromExperience = (workExperience: any[]): number => {
4343 return Math . round ( totalYears ) ;
4444} ;
4545
46- // ✅ Responsive Avatar Component
46+ // ✅ Responsive Avatar Component — X button visible only on hover
4747const TeamMemberAvatar = ( {
4848 imageUrl,
4949 name,
5050 position,
5151 size = 32 ,
5252 member,
53- onClick
53+ onClick,
54+ onRemove,
5455} : {
5556 imageUrl ?: string | null ;
5657 name : string ;
5758 position ?: string ;
5859 size ?: number ;
5960 member ?: Codev ;
6061 onClick ?: ( member : Codev ) => void ;
62+ onRemove ?: ( member : Codev ) => void ;
6163} ) => (
62- < div className = "flex flex-col items-center space-y-1 w-full" >
63- < div
64- className = { `relative flex-shrink-0 rounded-full overflow-hidden ring-1 sm:ring-2 ring-customBlue-400 ${
65- member && onClick ? 'cursor-pointer hover:ring-customBlue-300 transition-all duration-200' : ''
66- } `}
67- style = { { width : size , height : size } }
68- onClick = { ( e ) => {
69- e . stopPropagation ( ) ;
70- if ( member && onClick ) {
71- onClick ( member ) ;
72- }
73- } }
74- title = { member ? "Click to view profile" : undefined }
75- >
76- < img
77- src = {
78- imageUrl ||
79- "https://codebility-cdn.pages.dev/assets/images/default-avatar-200x200.jpg"
80- }
81- alt = { name }
82- className = "w-full h-full object-cover"
83- onError = { ( e ) => {
84- const target = e . target as HTMLImageElement ;
85- target . src = "https://codebility-cdn.pages.dev/assets/images/default-avatar-200x200.jpg" ;
64+ < div className = "flex flex-col items-center space-y-1 w-full" >
65+ { /* ✅ Extra padding so the X button (positioned outside the circle) is never clipped */ }
66+ < div className = "group relative" style = { { width : size + 8 , height : size + 8 , padding : 4 } } >
67+ < div
68+ className = { `relative rounded-full overflow-hidden ring-1 sm:ring-2 ring-customBlue-400 w-full h-full ${
69+ member && onClick ? 'cursor-pointer hover:ring-customBlue-300 transition-all duration-200' : ''
70+ } `}
71+ onClick = { ( e ) => {
72+ e . stopPropagation ( ) ;
73+ if ( member && onClick ) {
74+ onClick ( member ) ;
75+ }
8676 } }
87- />
77+ title = { member ? "Click to view profile" : undefined }
78+ >
79+ < img
80+ src = {
81+ imageUrl ||
82+ "https://codebility-cdn.pages.dev/assets/images/default-avatar-200x200.jpg"
83+ }
84+ alt = { name }
85+ className = "w-full h-full object-cover"
86+ onError = { ( e ) => {
87+ const target = e . target as HTMLImageElement ;
88+ target . src = "https://codebility-cdn.pages.dev/assets/images/default-avatar-200x200.jpg" ;
89+ } }
90+ />
91+ </ div >
92+
93+ { /* ✅ X Remove Button — only visible on hover via group-hover */ }
94+ { member && onRemove && (
95+ < button
96+ onClick = { ( e ) => {
97+ e . stopPropagation ( ) ;
98+ onRemove ( member ) ;
99+ } }
100+ title = "Remove member"
101+ className = "absolute top-0 right-0 z-20 flex items-center justify-center rounded-full bg-red-500 hover:bg-red-600 text-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity duration-150"
102+ style = { { width : Math . max ( 14 , size * 0.38 ) , height : Math . max ( 14 , size * 0.38 ) } }
103+ >
104+ < X style = { { width : Math . max ( 8 , size * 0.22 ) , height : Math . max ( 8 , size * 0.22 ) } } strokeWidth = { 3 } />
105+ </ button >
106+ ) }
88107 </ div >
108+
89109 < div className = "text-center w-full" >
90- < div className = "text-xs text-white font-medium truncate w-full leading-tight mt-2 " >
110+ < div className = "text-xs text-white font-medium truncate w-full leading-tight mt-1 " >
91111 { name }
92112 </ div >
93113 { position && (
@@ -180,15 +200,17 @@ const TeamLeaderDisplay = ({
180200 </ div >
181201) ;
182202
183- // ✅ Team Members Grid
203+ // ✅ Team Members Grid — flex-based scroll, hover-reveal scrollbar, no cut-offs
184204const TeamMembersGrid = ( {
185205 members,
186206 currentMemberIds,
187- onProfileClick
207+ onProfileClick,
208+ onRemoveMember,
188209} : {
189210 members : Codev [ ] ;
190211 currentMemberIds : string [ ] ;
191212 onProfileClick ?: ( member : Codev ) => void ;
213+ onRemoveMember ?: ( member : Codev ) => void ;
192214} ) => {
193215 const getAvatarSize = ( ) => {
194216 if ( typeof window !== 'undefined' ) {
@@ -200,32 +222,60 @@ const TeamMembersGrid = ({
200222 } ;
201223
202224 return (
203- < div className = "h-full flex flex-col" >
225+ < div className = "flex flex-col min-h-0 flex-1 " >
204226 < h4 className = "text-base sm:text-lg font-semibold text-white mb-1 mt-[-15px]" > Team Members</ h4 >
205- < div className = "mb-1 sm:mb-2 md:mt-2" >
227+ < div className = "mb-1 sm:mb-2 md:mt-2 flex-shrink-0 " >
206228 < span className = "text-white text-xs sm:text-sm" > { members . length } members</ span >
207229 </ div >
208- < div className = "grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-1 sm:gap-2 w-full mt-6" >
209- { members . map ( ( member ) => {
210- const isNew = ! currentMemberIds . includes ( member . id ) ;
211- return (
212- < div key = { member . id } className = "w-full flex flex-col items-center relative mb-5" >
213- { isNew && (
214- < div className = "absolute -top-1 -right-1 bg-green-500 text-white text-xs px-1 rounded-full z-10" >
215- New
216- </ div >
217- ) }
218- < TeamMemberAvatar
219- imageUrl = { member . image_url }
220- name = { member . first_name }
221- position = { member . display_position }
222- size = { getAvatarSize ( ) }
223- member = { member }
224- onClick = { onProfileClick }
225- />
226- </ div >
227- ) ;
228- } ) }
230+
231+ { /*
232+ - flex-1 + min-h-0 lets the scroll container grow to fill whatever space
233+ the parent left panel gives it — no hardcoded heights, no excess space
234+ - overflowY:'scroll' always reserves the scrollbar gutter → zero layout shift on hover
235+ - scrollbar track/thumb are transparent by default, visible only on hover via CSS
236+ - inner grid overflow:visible so X buttons and avatar rings never clip
237+ */ }
238+ < div
239+ className = "mt-2 members-scroll flex-1 min-h-0"
240+ style = { { overflowY : 'scroll' , overflowX : 'hidden' } }
241+ >
242+ < div
243+ className = "grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 w-full"
244+ style = { {
245+ columnGap : '8px' ,
246+ rowGap : '24px' ,
247+ paddingTop : '8px' ,
248+ paddingBottom : '16px' ,
249+ paddingLeft : '4px' ,
250+ paddingRight : '4px' ,
251+ overflow : 'visible' ,
252+ } }
253+ >
254+ { members . map ( ( member ) => {
255+ const isNew = ! currentMemberIds . includes ( member . id ) ;
256+ return (
257+ < div key = { member . id } className = "w-full flex flex-col items-center relative" >
258+ { isNew && (
259+ < div
260+ className = "absolute bg-green-500 text-white text-xs px-1 rounded-full z-30 pointer-events-none"
261+ style = { { top : 2 , right : 2 } }
262+ >
263+ New
264+ </ div >
265+ ) }
266+ < TeamMemberAvatar
267+ imageUrl = { member . image_url }
268+ name = { member . first_name }
269+ position = { member . display_position }
270+ size = { getAvatarSize ( ) }
271+ member = { member }
272+ onClick = { onProfileClick }
273+ onRemove = { onRemoveMember }
274+ />
275+ </ div >
276+ ) ;
277+ } ) }
278+ </ div >
229279 </ div >
230280 </ div >
231281 ) ;
@@ -237,15 +287,17 @@ const ProjectPreview = ({
237287 teamLead,
238288 selectedMembers,
239289 currentMemberIds,
240- onProfileClick
290+ onProfileClick,
291+ onRemoveMember,
241292} : {
242293 projectName : string ;
243294 teamLead : SimpleMemberData | null ;
244295 selectedMembers : Codev [ ] ;
245296 currentMemberIds : string [ ] ;
246297 onProfileClick ?: ( member : Codev ) => void ;
298+ onRemoveMember ?: ( member : Codev ) => void ;
247299} ) => (
248- < div className = "h-full flex flex-col p-1 sm:p-2 gap-y-6 " >
300+ < div className = "h-full flex flex-col p-1 sm:p-2 gap-y-4 overflow-hidden " >
249301 < div className = "flex-shrink-0" >
250302 < h3 className = "text-base sm:text-lg font-semibold text-white mb-1" > Project Name</ h3 >
251303 < div className = "flex items-center gap-2 sm:gap-3 mt-2" >
@@ -278,11 +330,12 @@ const ProjectPreview = ({
278330 />
279331 </ div >
280332
281- < div className = "flex-1 min-h-0 mr-8" >
333+ < div className = "flex-1 min-h-0 mr-8 flex flex-col " >
282334 < TeamMembersGrid
283335 members = { selectedMembers }
284336 currentMemberIds = { currentMemberIds }
285337 onProfileClick = { onProfileClick }
338+ onRemoveMember = { onRemoveMember }
286339 />
287340 </ div >
288341 </ div >
@@ -317,7 +370,7 @@ const AddMembersModal = ({
317370 const [ isLoadingMembers , setIsLoadingMembers ] = useState ( false ) ;
318371 const [ loadError , setLoadError ] = useState < string | null > ( null ) ;
319372
320- // ✅ FIXED: Paginated fetch to bypass Supabase's 1000-row default limit
373+ // ✅ Paginated fetch to bypass Supabase's 1000-row default limit
321374 const fetchAllCodevs = useCallback ( async ( ) : Promise < Codev [ ] > => {
322375 if ( ! supabase ) {
323376 console . error ( 'Supabase client not available' ) ;
@@ -346,7 +399,6 @@ const AddMembersModal = ({
346399
347400 allData = [ ...allData , ...data ] ;
348401
349- // If we got fewer rows than PAGE_SIZE, we've reached the last page
350402 if ( data . length < PAGE_SIZE ) break ;
351403
352404 from += PAGE_SIZE ;
@@ -532,6 +584,11 @@ const AddMembersModal = ({
532584 } ) ;
533585 } , [ ] ) ;
534586
587+ // ✅ Remove member via X button on avatar
588+ const handleRemoveMember = useCallback ( ( member : Codev ) => {
589+ setSelectedMembers ( prev => prev . filter ( m => m . id !== member . id ) ) ;
590+ } , [ ] ) ;
591+
535592 const isSelected = useCallback ( ( userId : string ) =>
536593 selectedMembers . some ( m => m . id === userId ) ,
537594 [ selectedMembers ]
@@ -644,6 +701,36 @@ const AddMembersModal = ({
644701 if ( ! isOpen ) return null ;
645702
646703 return (
704+ < >
705+ < style > { `
706+ /* Members grid: scrollbar gutter always reserved (no layout shift), thumb hidden until hover */
707+ .members-scroll {
708+ scrollbar-width: thin;
709+ scrollbar-color: transparent transparent;
710+ }
711+ .members-scroll:hover {
712+ scrollbar-color: rgba(99, 120, 255, 0.45) transparent;
713+ }
714+ .members-scroll::-webkit-scrollbar {
715+ width: 4px;
716+ background: transparent;
717+ }
718+ .members-scroll::-webkit-scrollbar-track {
719+ background: transparent;
720+ border: none;
721+ box-shadow: none;
722+ }
723+ .members-scroll::-webkit-scrollbar-thumb {
724+ background-color: transparent;
725+ border-radius: 4px;
726+ border: none;
727+ box-shadow: none;
728+ }
729+ .members-scroll:hover::-webkit-scrollbar-thumb {
730+ background-color: rgba(99, 120, 255, 0.45);
731+ }
732+ ` } </ style >
733+ < div style = { { display :'contents' } } >
647734 < Dialog open = { isOpen } onOpenChange = { onClose } >
648735 < DialogContent className = "max-w-full w-[85vw] sm:w-[80vw] lg:w-[80vw] h-[80vh] flex flex-col dark:bg-slate-950 border-2 border-slate-300 dark:border-blue-900/60 p-4 sm:p-0" >
649736 < DialogHeader className = "flex-shrink-0 px-3 sm:px-6 pt-1 pb-1" >
@@ -654,13 +741,14 @@ const AddMembersModal = ({
654741
655742 < div className = "flex flex-col lg:flex-row flex-1 min-h-0 overflow-hidden" >
656743 { /* Left Panel - Project Preview */ }
657- < div className = "w-full lg:w-3/5 border-b lg:border-b-0 lg:border-r border-white/30 dark:border-white/20 pr-0 md:pr-6" >
744+ < div className = "w-full lg:w-3/5 border-b lg:border-b-0 lg:border-r border-white/30 dark:border-white/20 pr-0 md:pr-6 h-full overflow-hidden " >
658745 < ProjectPreview
659746 projectName = { project . name }
660747 teamLead = { teamLeadData }
661748 selectedMembers = { selectedMembers }
662749 currentMemberIds = { currentMembers . map ( m => m . id ) }
663750 onProfileClick = { handleProfileClick }
751+ onRemoveMember = { handleRemoveMember }
664752 />
665753 </ div >
666754
@@ -821,6 +909,8 @@ const AddMembersModal = ({
821909 </ div >
822910 </ DialogContent >
823911 </ Dialog >
912+ </ div >
913+ </ >
824914 ) ;
825915} ;
826916
0 commit comments