Skip to content

Commit 690103e

Browse files
committed
Task: Improve registration ux (#256)
* feat: add required asterisk indicator and improve phone number input format * feat: enhance dropdown search with loading state and improved member filtering * feat: redesign thank you page with enhanced messaging and responsive layout * feat: implement form data persistence with localStorage and enhance registration flow * chore: remove unnecessary onMounted * chore: remove additonal unnecessary onMounted in the registration phases * chore: remove unused onMounted imports * chore: remove watch deep true for the persistance * feat: add a button to redirect the user to the home page * feat: remove timeout to clear the data and use the close registration event trigger instead
1 parent baec557 commit 690103e

File tree

8 files changed

+653
-47
lines changed

8 files changed

+653
-47
lines changed

components/Form/AppControlInput.vue

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,17 @@
1414
:type="props.inputType"
1515
class="checkbox form-check-input border border-gray-300 rounded-sm bg-white checked:bg-blue-600 checked:border-blue-600 focus:outline-none transition duration-200 mt-1 align-top bg-no-repeat bg-center bg-contain float-left mr-2 cursor-pointer"
1616
/>
17-
<label class="items-center" :for="props.labelId"> <slot /></label>
17+
<label class="items-center" :for="props.labelId">
18+
<slot />
19+
<span v-if="props.isRequired" class="required-asterisk">*</span>
20+
</label>
1821
</div>
1922
<div v-else>
2023
<div v-if="$props.showSlot">
21-
<label> <slot /></label>
24+
<label>
25+
<slot />
26+
<span v-if="props.isRequired" class="required-asterisk">*</span>
27+
</label>
2228
</div>
2329
<div
2430
v-if="props.inputType !== 'textarea' && props.inputType !== 'file'"
@@ -33,6 +39,7 @@
3339
:type="state.showPassword ? 'text' : props.inputType"
3440
:pattern="props.pattern"
3541
:value="props.value"
42+
:title="props.title"
3643
class="interactive-control"
3744
:class="props.showPasswordIcon ? 'hide-right-border' : ''"
3845
@focusin="state.pwActive = true"
@@ -62,6 +69,7 @@
6269
:placeholder="props.placeholder"
6370
:type="state.showPassword ? 'text' : props.inputType"
6471
:pattern="props.pattern"
72+
:title="props.title"
6573
class="interactive-control"
6674
:class="props.editIcon ? 'input-file-edit-icon' : ''"
6775
@focusin="state.pwActive = true"
@@ -74,6 +82,7 @@
7482
rows="5"
7583
@input="$emit('update:value', $event.target.value)"
7684
:required="props.isRequired"
85+
:title="props.title"
7786
>{{ props.value }}</textarea
7887
>
7988
</div>
@@ -96,7 +105,8 @@ const props = defineProps({
96105
value: { default: '' },
97106
width: { type: String, default: '' },
98107
labelId: { type: String, default: '' },
99-
acceptedFiles: {type: String, default: ''}
108+
acceptedFiles: {type: String, default: ''},
109+
title: { type: String, default: '' }
100110
})
101111
102112
const state = reactive({
@@ -191,4 +201,9 @@ input.input-file-edit-icon {
191201
.file-edit-container:hover {
192202
background-color: rgb(11 150 171 / 0.8);
193203
}
204+
205+
.required-asterisk {
206+
@apply text-community-red ml-1;
207+
font-weight: bold;
208+
}
194209
</style>

components/Form/AppDropDownSearch.vue

Lines changed: 159 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,22 @@
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
@@ -42,11 +47,15 @@
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
@@ -63,14 +72,16 @@
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
7687
const 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
87103
const 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
94204
const 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
111212
const 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>
120228
input {
@@ -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

Comments
 (0)