99  Code , 
1010  ActionIcon , 
1111  Tooltip , 
12+   Collapse , 
1213}  from  "@mantine/core" ; 
1314import  {  useClipboard  }  from  "@mantine/hooks" ; 
1415import  { 
@@ -17,12 +18,16 @@ import {
1718  IconCopy , 
1819  IconCheck , 
1920  IconMail , 
21+   IconAlertTriangle , 
22+   IconChevronDown , 
23+   IconChevronUp , 
2024}  from  "@tabler/icons-react" ; 
25+ import  {  illinoisNetId  }  from  "@common/types/generic" ; 
2126
2227interface  ResultSectionProps  { 
2328  title : string ; 
2429  items : string [ ] ; 
25-   color : "green"  |  "red" ; 
30+   color : "green"  |  "red"   |   "yellow" ; 
2631  icon : React . ReactNode ; 
2732  domain ?: string ; 
2833} 
@@ -34,6 +39,7 @@ const ResultSection = ({
3439  icon, 
3540  domain, 
3641} : ResultSectionProps )  =>  { 
42+   const  [ isOpen ,  setIsOpen ]  =  useState ( false ) ; 
3743  const  clipboardIds  =  useClipboard ( {  timeout : 1000  } ) ; 
3844  const  clipboardEmails  =  useClipboard ( {  timeout : 1000  } ) ; 
3945
@@ -56,6 +62,18 @@ const ResultSection = ({
5662    > 
5763      < Group  justify = "space-between"  mb = "xs" > 
5864        < Group  gap = "xs" > 
65+           < ActionIcon 
66+             variant = "transparent" 
67+             color = "white" 
68+             onClick = { ( )  =>  setIsOpen ( ( o )  =>  ! o ) } 
69+             aria-label = { isOpen  ? "Collapse section"  : "Expand section" } 
70+           > 
71+             { isOpen  ? ( 
72+               < IconChevronUp  size = { 20 }  /> 
73+             )  : ( 
74+               < IconChevronDown  size = { 20 }  /> 
75+             ) } 
76+           </ ActionIcon > 
5977          { icon } 
6078          < Title  order = { 5 }  c = "white" > 
6179            { title }  ({ items . length } )
@@ -92,22 +110,24 @@ const ResultSection = ({
92110          ) } 
93111        </ Group > 
94112      </ Group > 
95-       < Box > 
96-         { items . map ( ( item )  =>  ( 
97-           < Code 
98-             key = { item } 
99-             mr = { 5 } 
100-             mb = { 5 } 
101-             style = { { 
102-               display : "inline-block" , 
103-               color : "white" , 
104-               backgroundColor : "rgba(0, 0, 0, 0.25)" , 
105-             } } 
106-           > 
107-             { item } 
108-           </ Code > 
109-         ) ) } 
110-       </ Box > 
113+       < Collapse  in = { isOpen } > 
114+         < Box  pt = "xs"  pl = "xl" > 
115+           { items . map ( ( item ,  index )  =>  ( 
116+             < Code 
117+               key = { `${ item }  -${ index }  ` } 
118+               mr = { 5 } 
119+               mb = { 5 } 
120+               style = { { 
121+                 display : "inline-block" , 
122+                 color : "white" , 
123+                 backgroundColor : "rgba(0, 0, 0, 0.25)" , 
124+               } } 
125+             > 
126+               { item } 
127+             </ Code > 
128+           ) ) } 
129+         </ Box > 
130+       </ Collapse > 
111131    </ Box > 
112132  ) ; 
113133} ; 
@@ -117,7 +137,6 @@ interface MembershipListQueryProps {
117137    members : string [ ] ; 
118138    notMembers : string [ ] ; 
119139  } > ; 
120- 
121140  domain ?: string ; 
122141  inputLabel ?: string ; 
123142  inputDescription ?: string ; 
@@ -139,33 +158,93 @@ export const MembershipListQuery = ({
139158    members : string [ ] ; 
140159    notMembers : string [ ] ; 
141160  }  |  null > ( null ) ; 
161+   const  [ invalidEntries ,  setInvalidEntries ]  =  useState < string [ ] > ( [ ] ) ; 
142162
143163  const  handleQuery  =  async  ( )  =>  { 
144-     // Input processing logic remains the same 
145-     const  domainRegex  =  domain  ? new  RegExp ( `@${ domain }  $` ,  "i" )  : null ; 
146-     const  processedItems  =  input 
147-       . split ( / [ ; , \s \n ] + / ) 
148-       . map ( ( item )  =>  { 
149-         let  cleanItem  =  item . trim ( ) . toLowerCase ( ) ; 
150-         if  ( domainRegex )  { 
151-           cleanItem  =  cleanItem . replace ( domainRegex ,  "" ) ; 
164+     setIsLoading ( true ) ; 
165+     setResult ( null ) ; 
166+     setInvalidEntries ( [ ] ) ; 
167+ 
168+     const  rawItems  =  input . split ( / [ ; , \s \n ] + / ) . filter ( Boolean ) ; 
169+     const  validItemsForQuery  =  new  Set < string > ( ) ; 
170+ 
171+     const  allProcessedItems  =  rawItems . map ( ( item )  =>  { 
172+       const  trimmedItem  =  item . trim ( ) ; 
173+       let  potentialNetId  =  trimmedItem . toLowerCase ( ) ; 
174+       let  isValid  =  false ; 
175+       let  cleanedNetId  =  "" ; 
176+ 
177+       if  ( potentialNetId . includes ( "@" ) )  { 
178+         if  ( domain  &&  potentialNetId . endsWith ( `@${ domain }  ` ) )  { 
179+           potentialNetId  =  potentialNetId . replace ( `@${ domain }  ` ,  "" ) ; 
180+           if  ( illinoisNetId . safeParse ( potentialNetId ) . success )  { 
181+             isValid  =  true ; 
182+             cleanedNetId  =  potentialNetId ; 
183+           } 
152184        } 
153-         return  cleanItem ; 
154-       } ) 
155-       . filter ( Boolean ) ; 
185+       }  else  if  ( illinoisNetId . safeParse ( potentialNetId ) . success )  { 
186+         isValid  =  true ; 
187+         cleanedNetId  =  potentialNetId ; 
188+       } 
156189
157-     const  uniqueItems  =  [ ...new  Set ( processedItems ) ] ; 
158-     if  ( uniqueItems . length  ===  0 )  { 
190+       if  ( isValid )  { 
191+         validItemsForQuery . add ( cleanedNetId ) ; 
192+       } 
193+ 
194+       return  { 
195+         original : trimmedItem , 
196+         isValid, 
197+         cleaned : cleanedNetId , 
198+       } ; 
199+     } ) ; 
200+ 
201+     if  ( validItemsForQuery . size  ===  0 )  { 
202+       const  invalidItems  =  allProcessedItems 
203+         . filter ( ( p )  =>  ! p . isValid ) 
204+         . map ( ( p )  =>  p . original ) ; 
205+       setInvalidEntries ( 
206+         invalidItems . filter ( 
207+           ( item ,  index )  =>  invalidItems . indexOf ( item )  ===  index , 
208+         ) , 
209+       ) ; 
210+       setIsLoading ( false ) ; 
159211      return ; 
160212    } 
161213
162-     setIsLoading ( true ) ; 
163-     setResult ( null ) ; 
164- 
165214    try  { 
166-       const  queryResult  =  await  queryFunction ( uniqueItems ) ; 
215+       const  queryResult  =  await  queryFunction ( [ ...validItemsForQuery ] ) ; 
216+       const  memberSet  =  new  Set ( queryResult . members ) ; 
217+       const  orderedMembers : string [ ]  =  [ ] ; 
218+       const  orderedNotMembers : string [ ]  =  [ ] ; 
219+       const  orderedInvalid : string [ ]  =  [ ] ; 
220+ 
221+       allProcessedItems . forEach ( ( item )  =>  { 
222+         if  ( ! item . isValid )  { 
223+           orderedInvalid . push ( item . original ) ; 
224+         }  else  if  ( memberSet . has ( item . cleaned ) )  { 
225+           orderedMembers . push ( item . cleaned ) ; 
226+         }  else  { 
227+           orderedNotMembers . push ( item . cleaned ) ; 
228+         } 
229+       } ) ; 
230+ 
231+       // --- THIS IS THE CORRECTED DEDUPLICATION LOGIC --- 
232+       // For each list, keep only the first occurrence of each item. 
233+       const  uniqueMembers  =  orderedMembers . filter ( 
234+         ( item ,  index )  =>  orderedMembers . indexOf ( item )  ===  index , 
235+       ) ; 
236+       const  uniqueNotMembers  =  orderedNotMembers . filter ( 
237+         ( item ,  index )  =>  orderedNotMembers . indexOf ( item )  ===  index , 
238+       ) ; 
239+       const  uniqueInvalid  =  orderedInvalid . filter ( 
240+         ( item ,  index )  =>  orderedInvalid . indexOf ( item )  ===  index , 
241+       ) ; 
167242
168-       setResult ( queryResult ) ; 
243+       setResult ( { 
244+         members : uniqueMembers , 
245+         notMembers : uniqueNotMembers , 
246+       } ) ; 
247+       setInvalidEntries ( uniqueInvalid ) ; 
169248    }  catch  ( error )  { 
170249      console . error ( "An error occurred during the query:" ,  error ) ; 
171250    }  finally  { 
@@ -193,30 +272,46 @@ export const MembershipListQuery = ({
193272        { ctaText } 
194273      </ Button > 
195274
196-       { result  &&  ( 
197-         < Stack  gap = "md"  mt = "sm" > 
275+       < Stack  gap = "md"  mt = "sm" > 
276+         { result  &&  ( 
277+           < > 
278+             < ResultSection 
279+               title = "Paid Members" 
280+               items = { result . members } 
281+               color = "green" 
282+               icon = { 
283+                 < IconCircleCheck 
284+                   style = { {  color : "var(--mantine-color-white)"  } } 
285+                 /> 
286+               } 
287+               domain = { domain } 
288+             /> 
289+             { /* --- THIS LINE IS NOW FIXED --- */ } 
290+             < ResultSection 
291+               title = "Not Paid Members" 
292+               items = { result . notMembers } 
293+               color = "red" 
294+               icon = { 
295+                 < IconCircleX  style = { {  color : "var(--mantine-color-white)"  } }  /> 
296+               } 
297+               domain = { domain } 
298+             /> 
299+           </ > 
300+         ) } 
301+ 
302+         { invalidEntries . length  >  0  &&  ( 
198303          < ResultSection 
199-             title = "Paid Members " 
200-             items = { result . members } 
201-             color = "green " 
304+             title = "Invalid Entries " 
305+             items = { invalidEntries } 
306+             color = "yellow " 
202307            icon = { 
203-               < IconCircleCheck 
308+               < IconAlertTriangle 
204309                style = { {  color : "var(--mantine-color-white)"  } } 
205310              /> 
206311            } 
207-             domain = { domain } 
208-           /> 
209-           < ResultSection 
210-             title = "Not Paid Members" 
211-             items = { result . notMembers } 
212-             color = "red" 
213-             icon = { 
214-               < IconCircleX  style = { {  color : "var(--mantine-color-white)"  } }  /> 
215-             } 
216-             domain = { domain } 
217312          /> 
218-         </ Stack > 
219-       ) } 
313+         ) } 
314+       </ Stack > 
220315    </ Stack > 
221316  ) ; 
222317} ; 
0 commit comments