11"use client" ;
22
3- import { useState , useEffect , useRef } from "react" ;
3+ import { useState , useEffect , useMemo } from "react" ;
44import Modal from "./shared/Modal" ;
55import Button from "./shared/Button" ;
6- import Input from "./shared/Input " ;
6+ import UrlCombobox , { ComboboxOption } from "./shared/UrlCombobox " ;
77import { FileItemData } from "./FileItem" ;
88import { fetchUserContacts , Contact } from "../lib/helpers/contactUtils" ;
99import { fetchAndParseProfile , extractNameAndEmail } from "../lib/helpers/profileUtils" ;
@@ -35,16 +35,12 @@ export default function ShareDialog({
3535 const [ webIdInput , setWebIdInput ] = useState ( "" ) ;
3636 const [ contacts , setContacts ] = useState < Contact [ ] > ( [ ] ) ;
3737 const [ isLoadingContacts , setIsLoadingContacts ] = useState ( false ) ;
38- const [ showDropdown , setShowDropdown ] = useState ( false ) ;
39- const [ filteredContacts , setFilteredContacts ] = useState < Contact [ ] > ( [ ] ) ;
4038 const [ selectedAccessLevel , setSelectedAccessLevel ] = useState < AccessLevel > ( "Editor" ) ;
4139 const [ peopleChips , setPeopleChips ] = useState < PersonChip [ ] > ( [ ] ) ;
4240 const [ isAddingWebId , setIsAddingWebId ] = useState ( false ) ;
4341 const [ isSharing , setIsSharing ] = useState ( false ) ;
4442 const [ accessList , setAccessList ] = useState < Array < { webId : string ; accessModes : string [ ] } > | null > ( null ) ;
4543 const [ isLoadingAccessList , setIsLoadingAccessList ] = useState ( false ) ;
46- const inputRef = useRef < HTMLInputElement > ( null ) ;
47- const dropdownRef = useRef < HTMLDivElement > ( null ) ;
4844
4945 // Fetch contacts and access list when dialog opens
5046 useEffect ( ( ) => {
@@ -75,64 +71,34 @@ export default function ShareDialog({
7571 } else {
7672 // Reset state when dialog closes
7773 setWebIdInput ( "" ) ;
78- setShowDropdown ( false ) ;
79- setFilteredContacts ( [ ] ) ;
8074 setSelectedAccessLevel ( "Editor" ) ;
8175 setPeopleChips ( [ ] ) ;
8276 setAccessList ( null ) ;
8377 }
8478 } , [ isOpen , file ] ) ;
8579
86- // Filter contacts based on input
87- useEffect ( ( ) => {
88- if ( ! webIdInput . trim ( ) ) {
89- // When input is empty, show all contacts
90- setFilteredContacts ( contacts ) ;
91- return ;
92- }
93-
94- const query = webIdInput . toLowerCase ( ) . trim ( ) ;
95- const filtered = contacts . filter ( ( contact ) => {
96- const nameMatch = contact . name ?. toLowerCase ( ) . includes ( query ) ;
97- const emailMatch = contact . email ?. toLowerCase ( ) . includes ( query ) ;
98- const webIdMatch = contact . webId . toLowerCase ( ) . includes ( query ) ;
99- return nameMatch || emailMatch || webIdMatch ;
100- } ) ;
101-
102- setFilteredContacts ( filtered ) ;
103- } , [ webIdInput , contacts ] ) ;
104-
105- // Close dropdown when clicking outside
106- useEffect ( ( ) => {
107- const handleClickOutside = ( event : MouseEvent ) => {
108- if (
109- dropdownRef . current &&
110- ! dropdownRef . current . contains ( event . target as Node ) &&
111- inputRef . current &&
112- ! inputRef . current . contains ( event . target as Node )
113- ) {
114- setShowDropdown ( false ) ;
115- }
116- } ;
117-
118- if ( showDropdown ) {
119- document . addEventListener ( "mousedown" , handleClickOutside ) ;
120- return ( ) => document . removeEventListener ( "mousedown" , handleClickOutside ) ;
121- }
122- } , [ showDropdown ] ) ;
123-
124- const handleInputChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
125- setWebIdInput ( e . target . value ) ;
126- } ;
80+ // Convert contacts to ComboboxOptions
81+ const contactOptions : ComboboxOption [ ] = useMemo ( ( ) => {
82+ return contacts . map ( ( contact ) => ( {
83+ label : contact . name || contact . email || contact . webId ,
84+ value : contact . webId ,
85+ secondaryLabel : contact . email && contact . name ? contact . email : undefined ,
86+ icon : (
87+ < div className = "flex h-8 w-8 items-center justify-center rounded-full bg-gray-200" >
88+ { contact . name ? (
89+ < span className = "text-sm font-medium text-gray-700" >
90+ { contact . name . charAt ( 0 ) . toUpperCase ( ) }
91+ </ span >
92+ ) : (
93+ < UserIcon className = "h-5 w-5 text-gray-500" />
94+ ) }
95+ </ div >
96+ ) ,
97+ } ) ) ;
98+ } , [ contacts ] ) ;
12799
128- const handleContactSelect = ( contact : Contact ) => {
129- // Populate the input with the selected contact's WebID
130- setWebIdInput ( contact . webId ) ;
131- setShowDropdown ( false ) ;
132- // Focus back on input so user can press Enter to add
133- setTimeout ( ( ) => {
134- inputRef . current ?. focus ( ) ;
135- } , 0 ) ;
100+ const handleWebIdChange = ( value : string ) => {
101+ setWebIdInput ( value ) ;
136102 } ;
137103
138104 const handleAddWebId = async ( ) => {
@@ -144,7 +110,6 @@ export default function ShareDialog({
144110 // Check if person is already added
145111 if ( peopleChips . some ( ( p ) => p . webId === webId ) ) {
146112 setWebIdInput ( "" ) ;
147- setShowDropdown ( false ) ;
148113 return ;
149114 }
150115
@@ -171,7 +136,6 @@ export default function ShareDialog({
171136 } ,
172137 ] ) ;
173138 setWebIdInput ( "" ) ;
174- setShowDropdown ( false ) ;
175139 } catch ( error ) {
176140 console . error ( "Failed to fetch profile for WebID:" , error ) ;
177141 // Add with just WebID if profile fetch fails
@@ -184,7 +148,6 @@ export default function ShareDialog({
184148 } ,
185149 ] ) ;
186150 setWebIdInput ( "" ) ;
187- setShowDropdown ( false ) ;
188151 } finally {
189152 setIsAddingWebId ( false ) ;
190153 }
@@ -294,65 +257,18 @@ export default function ShareDialog({
294257 </ div >
295258 ) }
296259
297- < div className = "relative" >
298- < Input
299- ref = { inputRef }
300- type = "text"
301- placeholder = "Add a WebID"
302- value = { webIdInput }
303- onChange = { handleInputChange }
304- onFocus = { ( ) => {
305- // Show all contacts when input is focused
306- if ( contacts . length > 0 ) {
307- setShowDropdown ( true ) ;
308- }
309- } }
310- onKeyDown = { ( e ) => {
311- if ( e . key === "Enter" && webIdInput . trim ( ) && ! isAddingWebId ) {
312- e . preventDefault ( ) ;
313- handleAddWebId ( ) ;
314- }
315- } }
316- leftIcon = { < MagnifyingGlassIcon className = "h-5 w-5" /> }
317- className = "w-full"
318- disabled = { isAddingWebId }
319- />
320- { showDropdown && filteredContacts . length > 0 && (
321- < div
322- ref = { dropdownRef }
323- className = "absolute z-10 mt-1 w-full rounded-md border border-gray-200 bg-white shadow-lg max-h-60 overflow-auto"
324- >
325- { filteredContacts . map ( ( contact ) => (
326- < button
327- key = { contact . webId }
328- type = "button"
329- onClick = { ( ) => handleContactSelect ( contact ) }
330- className = "w-full px-4 py-2 text-left hover:bg-gray-100 flex items-center gap-3"
331- >
332- < div className = "flex h-8 w-8 items-center justify-center rounded-full bg-gray-200" >
333- { contact . name ? (
334- < span className = "text-sm font-medium text-gray-700" >
335- { contact . name . charAt ( 0 ) . toUpperCase ( ) }
336- </ span >
337- ) : (
338- < UserIcon className = "h-5 w-5 text-gray-500" />
339- ) }
340- </ div >
341- < div className = "flex-1 min-w-0" >
342- < div className = "text-sm font-medium text-gray-900 truncate" >
343- { contact . name || contact . email || contact . webId }
344- </ div >
345- { contact . email && contact . name && (
346- < div className = "text-xs text-gray-500 truncate" >
347- { contact . email }
348- </ div >
349- ) }
350- </ div >
351- </ button >
352- ) ) }
353- </ div >
354- ) }
355- </ div >
260+ < UrlCombobox
261+ value = { webIdInput }
262+ onChange = { handleWebIdChange }
263+ onSubmit = { handleAddWebId }
264+ options = { contactOptions }
265+ placeholder = "Add a WebID"
266+ disabled = { isAddingWebId }
267+ leftIcon = { < MagnifyingGlassIcon className = "h-5 w-5" /> }
268+ showChevron = { false }
269+ aria-label = "Add a WebID"
270+ inputClassName = "h-9"
271+ />
356272 </ div >
357273
358274 { /* General access section */ }
0 commit comments