2222 <div class =" dropdown-popover" >
2323 <form @submit.prevent =" onSubmit" >
2424 <input
25+ ref =" searchInput"
2526 class =" input-field"
2627 type =" text"
2728 :placeholder =" placeholderText"
2829 v-model =" state.searchQuery"
30+ @focus =" onFocus"
31+ @blur =" onBlur"
32+ @input =" onInput"
2933 />
3034 <svg
31- @submit =" onSubmit"
35+ @click =" onSubmit"
3236 xmlns =" http://www.w3.org/2000/svg"
3337 viewBox =" 0 0 24 24"
3438 width =" 24"
3539 height =" 24"
40+ class =" cursor-pointer"
3641 >
3742 <path fill =" none" d =" M0 0h24v24H0z" />
3843 <path
4247 </svg >
4348 </form >
4449 <div class =" options" :class =" state.isVisible ? 'visible' : 'invisible'" >
45- <ul >
50+ <div v-if =" state.loading" class =" loading p-4 text-center" >
51+ <div class =" loader mx-auto" ></div >
52+ <span class =" text-sm text-gray-500" >Loading members...</span >
53+ </div >
54+ <ul v-else-if =" filteredMembers.length" >
4655 <li
4756 @click =" selectedItem(member)"
48- v-for =" (member, index) in state.members "
49- :key =" `member-${index}`"
57+ v-for =" (member, index) in filteredMembers "
58+ :key =" `member-${member.id || index}`"
5059 class =" flex flex-row"
5160 >
5261 <img
6372 </span >
6473 </li >
6574 </ul >
75+ <div v-else-if =" state.searchQuery && !state.loading" class =" no-results p-4 text-center text-gray-500" >
76+ No members found matching "{{ state.searchQuery }}"
77+ </div >
6678 </div >
6779 </div >
6880 </div >
6981 </div >
7082</template >
7183<script setup>
72- import { reactive , computed } from ' vue'
73- const config = useRuntimeConfig ()
84+ import { reactive , computed , ref } from ' vue'
7485
7586// Define emit events
7687const emit = defineEmits ([' emitSelected' ])
@@ -80,9 +91,14 @@ const state = reactive({
8091 searchQuery: ' ' ,
8192 selectedItem: null ,
8293 isVisible: false ,
83- members: [],
94+ allMembers: [], // Cache all members
95+ loading: false ,
96+ membersLoaded: false , // Track if we've loaded members
97+ focusTimeout: null , // For debouncing blur events
8498})
8599
100+ const searchInput = ref (null )
101+
86102// Define passed props
87103const props = defineProps ({
88104 placeholderText: {
@@ -91,30 +107,122 @@ const props = defineProps({
91107 },
92108})
93109
110+ // Computed property for filtered members
111+ const filteredMembers = computed (() => {
112+ if (! state .searchQuery .trim ()) {
113+ return state .allMembers
114+ }
115+
116+ const query = state .searchQuery .toLowerCase ()
117+ return state .allMembers .filter (member => {
118+ const firstName = member .first_name_en ? .toLowerCase () || ' '
119+ const lastName = member .last_name_en ? .toLowerCase () || ' '
120+ const fullName = ` ${ firstName} ${ lastName} `
121+
122+ return firstName .includes (query) ||
123+ lastName .includes (query) ||
124+ fullName .includes (query)
125+ })
126+ })
127+
128+ // Load all members (called once on component mount or first focus)
129+ const loadAllMembers = async () => {
130+ if (state .membersLoaded || state .loading ) return
131+
132+ state .loading = true
133+
134+ try {
135+ // Use the correct paginated API endpoint
136+ const data = await $api (' /member/page/1?is_none_josa_member=false' )
137+ state .allMembers = data? .items || []
138+ state .membersLoaded = true
139+ } catch (error) {
140+ console .error (' Error loading members:' , error)
141+ state .allMembers = []
142+ } finally {
143+ state .loading = false
144+ }
145+ }
146+
147+ // Search for specific members (when user types and hits enter)
148+ const searchMembers = async (query ) => {
149+ if (! query .trim ()) {
150+ return
151+ }
152+
153+ state .loading = true
154+
155+ try {
156+ // Use the correct paginated API endpoint with name filter
157+ const data = await $api (` /member/page/1?is_none_josa_member=false&name=${ encodeURIComponent (query)} ` )
158+
159+ // Merge search results with cached members (avoid duplicates)
160+ const searchResults = data? .items || []
161+ const existingIds = new Set (state .allMembers .map (m => m .id ))
162+ const newMembers = searchResults .filter (m => ! existingIds .has (m .id ))
163+
164+ state .allMembers = [... state .allMembers , ... newMembers]
165+ } catch (error) {
166+ console .error (' Error searching members:' , error)
167+ } finally {
168+ state .loading = false
169+ }
170+ }
171+
172+ // Handle input focus - show dropdown and load members if needed
173+ const onFocus = async () => {
174+ // Clear any pending blur timeout
175+ if (state .focusTimeout ) {
176+ clearTimeout (state .focusTimeout )
177+ state .focusTimeout = null
178+ }
179+
180+ await loadAllMembers ()
181+ state .isVisible = true
182+ }
183+
184+ // Handle input blur - hide dropdown after a short delay
185+ const onBlur = () => {
186+ // Delay hiding to allow clicks on dropdown items
187+ state .focusTimeout = setTimeout (() => {
188+ state .isVisible = false
189+ }, 200 )
190+ }
191+
192+ // Handle input changes - filter existing members
193+ const onInput = () => {
194+ // Show dropdown if hidden and there's content
195+ if (! state .isVisible && state .searchQuery .trim ()) {
196+ state .isVisible = true
197+ }
198+
199+ // If dropdown is visible, we don't need to do anything else
200+ // The computed filteredMembers will handle the filtering
201+ }
202+
203+ // Handle form submission (Enter key) - search for more members
94204const onSubmit = async () => {
95- await $api (
96- ` /member/search?q=${ state .searchQuery } `
97- )
98- .then ((response ) => response .json ())
99- .then ((data ) => {
100- state .members = Object .assign ([], data)
101- })
102-
103- if (state .members .length ) {
205+ if (state .searchQuery .trim ()) {
206+ await searchMembers (state .searchQuery .trim ())
104207 state .isVisible = true
105- } else {
106- state .isVisible = false
107208 }
108209}
109210
110211// Event Callback function to assign the selected value and emit the value back
111212const selectedItem = (item ) => {
112213 state .selectedItem = item
113- state .isVisible = ! state . isVisible
214+ state .isVisible = false
114215 state .searchQuery = ' '
115216
217+ // Clear any pending blur timeout
218+ if (state .focusTimeout ) {
219+ clearTimeout (state .focusTimeout )
220+ state .focusTimeout = null
221+ }
222+
116223 emit (' emitSelected' , state .selectedItem )
117224}
225+
118226< / script>
119227< style lang= " postcss" scoped>
120228input {
@@ -142,6 +250,8 @@ input {
142250
143251 .options {
144252 border: 1px solid #e0dddb;
253+ max- height: 300px ;
254+ overflow- y: auto;
145255
146256 ul {
147257 @apply list- none;
@@ -157,6 +267,36 @@ input {
157267 li: hover {
158268 @apply bg- community- blue text- community- grey- light cursor- pointer;
159269 }
270+
271+ li: last- child {
272+ border- bottom: none;
273+ }
160274 }
275+
276+ .loading {
277+ @apply flex flex- col items- center gap- 2 ;
278+ }
279+
280+ .loader {
281+ @apply w- 6 h- 6 border- 2 border- gray- 300 border- t- community- blue rounded- full animate- spin;
282+ }
283+
284+ .no - results {
285+ @apply text- sm italic;
286+ }
287+ }
288+
289+ /* Ensure dropdown appears above other elements */
290+ .dropdown - popover {
291+ z- index: 1000 ;
292+ }
293+
294+ /* Hide dropdown when invisible */
295+ .options .invisible {
296+ @apply hidden;
297+ }
298+
299+ .options .visible {
300+ @apply block;
161301}
162302< / style>
0 commit comments