Skip to content

Commit acbb944

Browse files
committed
refactor(contact): move ContactForm.vue and formSchema.js into features/contact/ for a better structure
1 parent cd429b9 commit acbb944

File tree

5 files changed

+139
-160
lines changed

5 files changed

+139
-160
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<template>
2+
<form @submit.prevent="onSubmit" class="space-y-8">
3+
<div class="flex flex-wrap gap-6">
4+
<div class="flex-1">
5+
<FormField v-slot="{ componentField }" name="first_name">
6+
<FormItem class="flex flex-col">
7+
<FormLabel class="flex items-center">{{ t('form.field.firstName') }}</FormLabel>
8+
<FormControl><Input v-bind="componentField" type="text" /></FormControl>
9+
<FormMessage />
10+
</FormItem>
11+
</FormField>
12+
</div>
13+
14+
<div class="flex-1">
15+
<FormField v-slot="{ componentField }" name="last_name">
16+
<FormItem class="flex flex-col">
17+
<FormLabel class="flex items-center">{{ t('form.field.lastName') }}</FormLabel>
18+
<FormControl><Input v-bind="componentField" type="text" /></FormControl>
19+
<FormMessage />
20+
</FormItem>
21+
</FormField>
22+
</div>
23+
</div>
24+
25+
<FormField v-slot="{ componentField }" name="avatar_url">
26+
<FormItem
27+
><FormControl><Input v-bind="componentField" type="hidden" /></FormControl
28+
></FormItem>
29+
</FormField>
30+
31+
<div class="flex flex-wrap gap-6">
32+
<div class="flex-1">
33+
<FormField v-slot="{ componentField }" name="email">
34+
<FormItem class="flex flex-col">
35+
<FormLabel class="flex items-center">{{ t('form.field.email') }}</FormLabel>
36+
<FormControl><Input v-bind="componentField" type="email" /></FormControl>
37+
<FormMessage />
38+
</FormItem>
39+
</FormField>
40+
</div>
41+
42+
<div class="flex flex-col flex-1">
43+
<div class="flex items-end">
44+
<FormField v-slot="{ componentField }" name="phone_number_calling_code">
45+
<FormItem class="w-20">
46+
<FormLabel class="flex items-center whitespace-nowrap">
47+
{{ t('form.field.phoneNumber') }}
48+
</FormLabel>
49+
<FormControl>
50+
<ComboBox
51+
v-bind="componentField"
52+
:items="allCountries"
53+
:placeholder="t('form.field.select')"
54+
:buttonClass="'rounded-r-none border-r-0'"
55+
>
56+
<template #item="{ item }">
57+
<div class="flex items-center gap-2">
58+
<div class="w-7 h-7 flex items-center justify-center">
59+
<span v-if="item.emoji">{{ item.emoji }}</span>
60+
</div>
61+
<span class="text-sm">{{ item.label }} ({{ item.value }})</span>
62+
</div>
63+
</template>
64+
65+
<template #selected="{ selected }">
66+
<div class="flex items-center mb-1">
67+
<span v-if="selected" class="text-xl leading-none">{{ selected.emoji }}</span>
68+
</div>
69+
</template>
70+
</ComboBox>
71+
</FormControl>
72+
<FormMessage />
73+
</FormItem>
74+
</FormField>
75+
76+
<div class="flex-1">
77+
<FormField v-slot="{ componentField }" name="phone_number">
78+
<FormItem class="relative">
79+
<FormControl>
80+
<Input
81+
type="tel"
82+
v-bind="componentField"
83+
class="rounded-l-none"
84+
inputmode="numeric"
85+
/>
86+
<FormMessage class="absolute top-full mt-1 text-sm" />
87+
</FormControl>
88+
</FormItem>
89+
</FormField>
90+
</div>
91+
</div>
92+
</div>
93+
</div>
94+
95+
<div>
96+
<Button type="submit" :isLoading="formLoading" :disabled="formLoading">
97+
{{ t('globals.buttons.update', { name: t('globals.terms.contact').toLowerCase() }) }}
98+
</Button>
99+
</div>
100+
</form>
101+
</template>
102+
103+
<script setup>
104+
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
105+
import { Input } from '@/components/ui/input'
106+
import { Button } from '@/components/ui/button'
107+
import ComboBox from '@/components/ui/combobox/ComboBox.vue'
108+
import countries from '@/constants/countries.js'
109+
import { useI18n } from 'vue-i18n'
110+
111+
defineProps(['formLoading', 'onSubmit'])
112+
113+
const { t } = useI18n()
114+
115+
const allCountries = countries.map((country) => ({
116+
label: country.name,
117+
value: country.calling_code,
118+
emoji: country.emoji
119+
}))
120+
</script>

frontend/src/features/contacts/ContactsList.vue renamed to frontend/src/features/contact/ContactsList.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ const totalPages = ref(0)
196196
const searchTerm = ref('')
197197
const orderByField = ref('users.created_at')
198198
const orderByDirection = ref('desc')
199+
const total = ref(0)
199200
const emitter = useEmitter()
200201
201202
// Google-style pagination
@@ -242,6 +243,7 @@ const fetchContacts = async () => {
242243
})
243244
contacts.value = response.data.data.results
244245
totalPages.value = response.data.data.total_pages
246+
total.value = response.data.data.total
245247
} catch (error) {
246248
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
247249
variant: 'destructive',

frontend/src/views/contact/ContactDetailView.vue

Lines changed: 16 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@
66
</div>
77

88
<div v-if="contact" class="flex justify-center space-y-4 w-full">
9-
<!-- Card -->
109
<div class="flex flex-col w-full">
1110
<div class="h-16"></div>
1211

1312
<div class="flex flex-col space-y-2">
14-
<!-- Avatar with upload-->
1513
<AvatarUpload
1614
@upload="onUpload"
1715
@remove="onRemove"
@@ -45,146 +43,23 @@
4543
</div>
4644

4745
<div class="mt-12">
48-
<form @submit.prevent="onSubmit" class="space-y-8">
49-
<div class="flex flex-wrap gap-6">
50-
<div class="flex-1">
51-
<FormField v-slot="{ componentField }" name="first_name">
52-
<FormItem class="flex flex-col">
53-
<FormLabel class="flex items-center">
54-
{{ t('form.field.firstName') }}
55-
</FormLabel>
56-
<FormControl>
57-
<Input v-bind="componentField" type="text" />
58-
</FormControl>
59-
<FormMessage />
60-
</FormItem>
61-
</FormField>
62-
</div>
63-
64-
<div class="flex-1">
65-
<FormField v-slot="{ componentField }" name="last_name" class="flex-1">
66-
<FormItem class="flex flex-col">
67-
<FormLabel class="flex items-center">
68-
{{ t('form.field.lastName') }}
69-
</FormLabel>
70-
<FormControl>
71-
<Input v-bind="componentField" type="text" />
72-
</FormControl>
73-
<FormMessage />
74-
</FormItem>
75-
</FormField>
76-
</div>
77-
</div>
78-
79-
<FormField v-slot="{ componentField }" name="avatar_url">
80-
<FormItem>
81-
<FormControl>
82-
<Input v-bind="componentField" type="hidden" />
83-
</FormControl>
84-
</FormItem>
85-
</FormField>
86-
87-
<div class="flex flex-wrap gap-6">
88-
<div class="flex-1">
89-
<FormField v-slot="{ componentField }" name="email">
90-
<FormItem class="flex flex-col">
91-
<FormLabel class="flex items-center">
92-
{{ t('form.field.email') }}
93-
</FormLabel>
94-
<FormControl>
95-
<Input v-bind="componentField" type="email" />
96-
</FormControl>
97-
<FormMessage />
98-
</FormItem>
99-
</FormField>
100-
</div>
101-
102-
<div class="flex flex-col flex-1">
103-
<div class="flex items-end">
104-
<FormField v-slot="{ componentField }" name="phone_number_calling_code">
105-
<FormItem class="w-20">
106-
<FormLabel class="flex items-center whitespace-nowrap">
107-
{{ t('form.field.phoneNumber') }}
108-
</FormLabel>
109-
<FormControl>
110-
<ComboBox
111-
v-bind="componentField"
112-
:items="allCountries"
113-
:placeholder="t('form.field.select')"
114-
:buttonClass="'rounded-r-none border-r-0'"
115-
>
116-
<template #item="{ item }">
117-
<div class="flex items-center gap-2">
118-
<div class="w-7 h-7 flex items-center justify-center">
119-
<span v-if="item.emoji">{{ item.emoji }}</span>
120-
</div>
121-
<span class="text-sm">{{ item.label }} ( {{ item.value }})</span>
122-
</div>
123-
</template>
124-
125-
<template #selected="{ selected }">
126-
<div class="flex items-center mb-1">
127-
<span v-if="selected" class="text-xl leading-none">
128-
{{ selected.emoji }}
129-
</span>
130-
</div>
131-
</template>
132-
</ComboBox>
133-
</FormControl>
134-
<FormMessage />
135-
</FormItem>
136-
</FormField>
137-
138-
<div class="flex-1">
139-
<FormField v-slot="{ componentField }" name="phone_number">
140-
<FormItem class="relative">
141-
<FormControl>
142-
<!-- Input field -->
143-
<Input
144-
type="tel"
145-
v-bind="componentField"
146-
class="rounded-l-none"
147-
inputmode="numeric"
148-
/>
149-
<FormMessage class="absolute top-full mt-1 text-sm" />
150-
</FormControl>
151-
</FormItem>
152-
</FormField>
153-
</div>
154-
</div>
155-
</div>
156-
</div>
157-
158-
<div>
159-
<Button type="submit" :isLoading="formLoading" :disabled="formLoading">
160-
{{
161-
$t('globals.buttons.update', {
162-
name: $t('globals.terms.contact').toLowerCase()
163-
})
164-
}}
165-
</Button>
166-
</div>
167-
</form>
46+
<ContactForm :formLoading="formLoading" :onSubmit="onSubmit" />
16847
</div>
16948
</div>
17049
</div>
17150

172-
<!-- Loading state -->
17351
<Spinner v-else />
17452

175-
<!-- Block/Unblock confirmation dialog -->
17653
<Dialog :open="showBlockConfirmation" @update:open="showBlockConfirmation = $event">
17754
<DialogContent class="sm:max-w-md">
17855
<DialogHeader>
179-
<DialogTitle>{{
180-
contact?.enabled
181-
? t('globals.buttons.block', {
182-
name: t('globals.terms.contact')
183-
})
184-
: t('globals.buttons.unblock', {
185-
name: t('globals.terms.contact')
186-
})
187-
}}</DialogTitle>
56+
<DialogTitle>
57+
{{
58+
contact?.enabled
59+
? t('globals.buttons.block', { name: t('globals.terms.contact') })
60+
: t('globals.buttons.unblock', { name: t('globals.terms.contact') })
61+
}}
62+
</DialogTitle>
18863
<DialogDescription>
18964
{{ contact?.enabled ? t('contact.blockConfirm') : t('contact.unblockConfirm') }}
19065
</DialogDescription>
@@ -215,8 +90,6 @@ import { useForm } from 'vee-validate'
21590
import { toTypedSchema } from '@vee-validate/zod'
21691
import { AvatarUpload } from '@/components/ui/avatar'
21792
import { Button } from '@/components/ui/button'
218-
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
219-
import { Input } from '@/components/ui/input'
22093
import {
22194
Dialog,
22295
DialogContent,
@@ -227,44 +100,31 @@ import {
227100
import { ShieldOffIcon, ShieldCheckIcon } from 'lucide-vue-next'
228101
import ContactDetail from '@/layouts/contact/ContactDetail.vue'
229102
import api from '@/api'
230-
import { createFormSchema } from './formSchema.js'
103+
import ContactForm from '@/features/contact/ContactForm.vue'
104+
import { createFormSchema } from '@/features/contact/formSchema.js'
231105
import { useEmitter } from '@/composables/useEmitter'
232106
import { EMITTER_EVENTS } from '@/constants/emitterEvents'
233107
import { handleHTTPError } from '@/utils/http'
234108
import { CustomBreadcrumb } from '@/components/ui/breadcrumb'
235109
import { Spinner } from '@/components/ui/spinner'
236-
import ComboBox from '@/components/ui/combobox/ComboBox.vue'
237-
import countries from '@/constants/countries.js'
238110
239111
const { t } = useI18n()
240112
const emitter = useEmitter()
241113
const route = useRoute()
242114
const formLoading = ref(false)
243115
const contact = ref(null)
244116
const showBlockConfirmation = ref(false)
117+
245118
const form = useForm({
246119
validationSchema: toTypedSchema(createFormSchema(t))
247120
})
248121
249-
const allCountries = countries.map((country) => ({
250-
label: country.name,
251-
value: country.calling_code,
252-
emoji: country.emoji
253-
}))
254-
255122
const breadcrumbLinks = [
256123
{ path: 'contacts', label: t('globals.terms.contact', 2) },
257-
{
258-
path: '',
259-
label: t('globals.messages.edit', {
260-
name: t('globals.terms.contact')
261-
})
262-
}
124+
{ path: '', label: t('globals.messages.edit', { name: t('globals.terms.contact') }) }
263125
]
264126
265-
onMounted(() => {
266-
fetchContact()
267-
})
127+
onMounted(fetchContact)
268128
269129
async function fetchContact() {
270130
try {
@@ -278,9 +138,8 @@ async function fetchContact() {
278138
279139
const getInitials = computed(() => {
280140
if (!contact.value) return ''
281-
const firstName = contact.value.first_name || ''
282-
const lastName = contact.value.last_name || ''
283-
return `${firstName.charAt(0).toUpperCase()}${lastName.charAt(0).toUpperCase()}`
141+
const { first_name = '', last_name = '' } = contact.value
142+
return `${first_name.charAt(0).toUpperCase()}${last_name.charAt(0).toUpperCase()}`
284143
})
285144
286145
async function confirmToggleBlock() {
@@ -305,9 +164,7 @@ async function toggleBlock() {
305164
const onSubmit = form.handleSubmit(async (values) => {
306165
try {
307166
formLoading.value = true
308-
await api.updateContact(contact.value.id, {
309-
...values
310-
})
167+
await api.updateContact(contact.value.id, { ...values })
311168
await fetchContact()
312169
emitToast(t('globals.messages.updatedSuccessfully', { name: t('globals.terms.contact') }))
313170
} catch (err) {

frontend/src/views/contact/ContactsView.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
</ContactList>
55
</template>
66
<script setup>
7-
import ContactsList from '@/features/contacts/ContactsList.vue'
7+
import ContactsList from '@/features/contact/ContactsList.vue'
88
import ContactList from '@/layouts/contact/ContactList.vue'
99
</script>

0 commit comments

Comments
 (0)