nuxt-layer-core 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,283 +1,285 @@
1
- <script setup lang="ts">
2
- const props = withDefaults(defineProps<{
3
- plans?: Array<{
4
- id: string
5
- name: string
6
- level: string
7
- duration: string
8
- icon: string
9
- price?: string
10
- description?: string
11
- }>
12
- selectedPlanId?: string
13
- }>(), {
14
- plans: () => [],
15
- })
16
-
17
- const { addOpportunity } = useCrmApi()
18
-
19
- // Form state
20
- const selectedCourseId = ref(props.selectedPlanId || '')
21
- const form = reactive({ name: '', phone: '', email: '', note: '' })
22
- const studyMode = ref('online')
23
- const errors = reactive({ name: '', phone: '', email: '', course: '' })
24
- const touched = reactive({ name: false, phone: false, email: false, course: false })
25
- const submitting = ref(false)
26
- const submitSuccess = ref(false)
27
-
28
- // Auto-select first plan if none selected
29
- watch(() => props.plans, (list: any) => {
30
- if (!selectedCourseId.value && list.length > 0) {
31
- selectedCourseId.value = list[0].id
32
- }
33
- }, { immediate: true })
34
-
35
- // Sync when selectedPlanId prop changes
36
- watch(() => props.selectedPlanId, (id) => {
37
- if (id) selectedCourseId.value = id
38
- })
39
-
40
- const selectedPlan = computed(() => props.plans.find((p) => p.id === selectedCourseId.value))
41
-
42
- // Validation
43
- function touch(field: keyof typeof touched) {
44
- touched[field] = true
45
- validateField(field)
46
- }
47
-
48
- function validateField(field: keyof typeof touched) {
49
- if (field === 'name') {
50
- if (!form.name) errors.name = 'Vui lòng nhập họ tên'
51
- else if (form.name.length < 2) errors.name = 'Họ tên phải có ít nhất 2 ký tự'
52
- else errors.name = ''
53
- }
54
- if (field === 'phone') {
55
- if (!form.phone) errors.phone = 'Vui lòng nhập số điện thoại'
56
- else if (!/^(0|\+84)\d{9,10}$/.test(form.phone.replace(/\s/g, ''))) errors.phone = 'Số điện thoại không hợp lệ'
57
- else errors.phone = ''
58
- }
59
- if (field === 'email') {
60
- if (form.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) errors.email = 'Email không hợp lệ'
61
- else errors.email = ''
62
- }
63
- if (field === 'course') {
64
- if (!selectedCourseId.value) errors.course = 'Vui lòng chọn khóa học'
65
- else errors.course = ''
66
- }
67
- }
68
-
69
- function formatLevel(level?: string) {
70
- const labels: Record<string, string> = {
71
- basic: 'Cơ bản',
72
- advanced: 'Nâng cao',
73
- expert: 'Chuyên gia',
74
- }
75
-
76
- return labels[level || ''] || level || ''
77
- }
78
-
79
- // Ghép level + duration, bỏ qua phần nào rỗng thay vì để dính dấu "·" trơ
80
- function planMetaLine(plan: { level?: string; duration?: string }) {
81
- return [formatLevel(plan.level), plan.duration].filter(Boolean).join(' · ')
82
- }
83
-
84
- function validateAll() {
85
- ; (['name', 'phone', 'email', 'course'] as const).forEach((f) => {
86
- touched[f] = true
87
- validateField(f)
88
- })
89
- return !errors.name && !errors.phone && !errors.email && !errors.course
90
- }
91
-
92
- // Submit
93
- async function handleSubmit() {
94
- if (!validateAll()) return
95
- submitting.value = true
96
- submitSuccess.value = false
97
-
98
- try {
99
- const hostname = window.location.origin
100
- const studyModeLabel =
101
- studyMode.value === 'online'
102
- ? 'Học trực tuyến (Online)'
103
- : studyMode.value === 'offline'
104
- ? 'Học tại lớp (Offline)'
105
- : 'Đào tạo doanh nghiệp'
106
-
107
- const levelLabel =
108
- selectedPlan.value?.level === 'basic'
109
- ? 'Cơ bản'
110
- : selectedPlan.value?.level === 'advanced'
111
- ? 'Nâng cao'
112
- : selectedPlan.value?.level === 'expert'
113
- ? 'Chuyên gia'
114
- : selectedPlan.value?.level || ''
115
-
116
-
117
- const description = [
118
- `<strong>Khóa học:</strong> ${selectedPlan.value?.name || ''}`,
119
- `<strong>Trình độ:</strong> ${levelLabel}`,
120
- `<strong>Thời lượng:</strong> ${selectedPlan.value?.duration || ''}`,
121
- `<strong>Hình thức học:</strong> ${studyModeLabel}`,
122
- `<strong>Họ và tên:</strong> ${form.name}`,
123
- `<strong>Số điện thoại:</strong> ${form.phone}`,
124
- `<strong>Email:</strong> ${form.email || 'Không cung cấp'}`,
125
- `<strong>Ghi chú:</strong> ${form.note || 'Không có'}`,
126
- ].join('<br>')
127
-
128
- await addOpportunity({
129
- name: `Yêu cầu đăng ký khóa học ${selectedPlan.value?.name}`,
130
- description: description,
131
- referName: form.name,
132
- referPhone: form.phone,
133
- referEmail: form.email,
134
- targetUrl: `${hostname}${window.location.pathname}`,
135
- extSource: 'WEBSITE',
136
- })
137
- submitSuccess.value = true
138
-
139
- setTimeout(() => {
140
- submitSuccess.value = false
141
- }, 2000)
142
- form.name = ''
143
- form.phone = ''
144
- form.email = ''
145
- form.note = ''
146
- Object.keys(touched).forEach((k) => (touched[k as keyof typeof touched] = false))
147
- } catch (err) {
148
- console.error('Error submitting registration:', err)
149
- errors.name = 'Gửi đăng ký thất bại, vui lòng thử lại.'
150
- touched.name = true
151
- } finally {
152
- submitting.value = false
153
- }
154
- }
155
- </script>
156
-
157
- <template>
158
- <section class="rounded-2xl border border-[#e7e9f0] bg-white p-8">
159
- <h2 class="mb-5 text-[17px] font-bold text-slate-900">Thông tin đăng ký</h2>
160
-
161
- <form @submit.prevent="handleSubmit">
162
- <!-- Course selection -->
163
- <div class="mb-5">
164
- <label class="mb-[10px] block text-[13px] font-semibold text-gray-700">
165
- {{ plans.length === 1 ? 'Gói bạn đăng ký tư vấn' : 'Chọn khóa học' }}
166
- </label>
167
-
168
- <!-- Đã chọn sẵn 1 gói từ trang trước -> chỉ hiển thị thông tin, không cần chọn lại -->
169
- <div v-if="plans.length === 1" class="flex flex-col gap-2 rounded-[10px] border border-[#2f5fe0] bg-[#eef3ff] px-4 py-[14px]">
170
- <div class="flex items-center gap-3">
171
- <span class="relative h-[18px] w-[18px] flex-none rounded-[10px] border-2 border-[#2f5fe0]">
172
- <span class="absolute inset-[3px] rounded-[10px] bg-[#2f5fe0]" />
173
- </span>
174
- <span class="flex flex-col gap-[2px]">
175
- <span class="text-sm font-semibold text-slate-900">{{ plans[0].name }}</span>
176
- <span v-if="planMetaLine(plans[0])" class="text-[12.5px] text-gray-500">{{ planMetaLine(plans[0]) }}</span>
177
- </span>
178
- </div>
179
- <p v-if="plans[0].price" class="pl-[30px] text-[12.5px] font-semibold text-[#2f5fe0]">{{ plans[0].price }}</p>
180
- <p v-if="plans[0].description" class="pl-[30px] text-[12.5px] text-gray-500">{{ plans[0].description }}</p>
181
- </div>
182
-
183
- <div v-else class="flex flex-col gap-[10px]">
184
- <label v-for="course in plans" :key="course.id"
185
- class="relative flex cursor-pointer items-center gap-3 rounded-[10px] border px-4 py-[14px] transition-colors"
186
- :class="selectedCourseId === course.id ? 'border-[#2f5fe0] bg-[#eef3ff]' : 'border-[#e2e5ee] bg-white'">
187
- <input type="radio" name="course" class="absolute h-0 w-0 opacity-0" :value="course.id"
188
- v-model="selectedCourseId" @change="touch('course')">
189
- <span class="relative h-[18px] w-[18px] flex-none rounded-[10px] border-2 transition-colors"
190
- :class="selectedCourseId === course.id ? 'border-[#2f5fe0]' : 'border-[#cbd2e1]'">
191
- <span v-if="selectedCourseId === course.id" class="absolute inset-[3px] rounded-[10px] bg-[#2f5fe0]" />
192
- </span>
193
- <span class="flex flex-col gap-[2px]">
194
- <span class="text-sm font-semibold text-slate-900">{{ course.name }}</span>
195
- <span v-if="planMetaLine(course)" class="text-[12.5px] text-gray-500">{{ planMetaLine(course) }}</span>
196
- </span>
197
- </label>
198
- </div>
199
- <p v-if="touched.course && errors.course" class="mt-1 text-[12px] text-red-500">{{ errors.course }}</p>
200
- </div>
201
-
202
- <!-- Name & Phone -->
203
- <div class="mb-5 grid grid-cols-1 gap-4 sm:grid-cols-2">
204
- <div>
205
- <label class="mb-[10px] block text-[13px] font-semibold text-gray-700" for="fullname">Họ và tên</label>
206
- <input id="fullname" v-model="form.name" type="text"
207
- class="w-full rounded-[10px] border px-[14px] py-[11px] text-sm text-slate-900 placeholder:text-gray-400 focus:outline-none focus:ring-[3px]"
208
- :class="touched.name && errors.name ? 'border-red-400 focus:border-red-400 focus:ring-red-400/15' : 'border-[#e2e5ee] focus:border-[#2f5fe0] focus:ring-[#2f5fe0]/15'"
209
- placeholder="Nguyễn Văn A" @blur="touch('name')">
210
- <p v-if="touched.name && errors.name" class="mt-1 text-[12px] text-red-500">{{ errors.name }}</p>
211
- </div>
212
- <div>
213
- <label class="mb-[10px] block text-[13px] font-semibold text-gray-700" for="phone">Số điện thoại</label>
214
- <input id="phone" v-model="form.phone" type="tel"
215
- class="w-full rounded-[10px] border px-[14px] py-[11px] text-sm text-slate-900 placeholder:text-gray-400 focus:outline-none focus:ring-[3px]"
216
- :class="touched.phone && errors.phone ? 'border-red-400 focus:border-red-400 focus:ring-red-400/15' : 'border-[#e2e5ee] focus:border-[#2f5fe0] focus:ring-[#2f5fe0]/15'"
217
- placeholder="0912 345 678" @blur="touch('phone')">
218
- <p v-if="touched.phone && errors.phone" class="mt-1 text-[12px] text-red-500">{{ errors.phone }}</p>
219
- </div>
220
- </div>
221
-
222
- <!-- Email -->
223
- <div class="mb-5">
224
- <label class="mb-[10px] block text-[13px] font-semibold text-gray-700" for="email">Email</label>
225
- <input id="email" v-model="form.email" type="email"
226
- class="w-full rounded-[10px] border px-[14px] py-[11px] text-sm text-slate-900 placeholder:text-gray-400 focus:outline-none focus:ring-[3px]"
227
- :class="touched.email && errors.email ? 'border-red-400 focus:border-red-400 focus:ring-red-400/15' : 'border-[#e2e5ee] focus:border-[#2f5fe0] focus:ring-[#2f5fe0]/15'"
228
- placeholder="example@congty.vn" @blur="touch('email')">
229
- <p v-if="touched.email && errors.email" class="mt-1 text-[12px] text-red-500">{{ errors.email }}</p>
230
- </div>
231
-
232
- <!-- Study mode -->
233
- <div class="mb-5">
234
- <label class="mb-[10px] block text-[13px] font-semibold text-gray-700">Hình thức học</label>
235
- <div class="flex flex-wrap gap-[10px]">
236
- <button type="button" class="rounded-[10px] border px-4 py-[9px] text-[13.5px] font-medium transition-colors"
237
- :class="studyMode === 'online' ? 'border-[#2f5fe0] bg-[#2f5fe0] text-white' : 'border-[#e2e5ee] bg-white text-gray-700'"
238
- @click="studyMode = 'online'">
239
- Học trực tuyến (Online)
240
- </button>
241
- <button type="button" class="rounded-[10px] border px-4 py-[9px] text-[13.5px] font-medium transition-colors"
242
- :class="studyMode === 'offline' ? 'border-[#2f5fe0] bg-[#2f5fe0] text-white' : 'border-[#e2e5ee] bg-white text-gray-700'"
243
- @click="studyMode = 'offline'">
244
- Học tại lớp (Offline)
245
- </button>
246
- <button type="button" class="rounded-[10px] border px-4 py-[9px] text-[13.5px] font-medium transition-colors"
247
- :class="studyMode === 'enterprise' ? 'border-[#2f5fe0] bg-[#2f5fe0] text-white' : 'border-[#e2e5ee] bg-white text-gray-700'"
248
- @click="studyMode = 'enterprise'">
249
- Đào tạo doanh nghiệp
250
- </button>
251
- </div>
252
- </div>
253
-
254
- <!-- Notes -->
255
- <div class="mb-5">
256
- <label class="mb-[10px] block text-[13px] font-semibold text-gray-700" for="note">Ghi chú (tùy chọn)</label>
257
- <textarea id="note" v-model="form.note" rows="3"
258
- class="w-full min-h-[84px] resize-y rounded-[10px] border border-[#e2e5ee] bg-[#fafafb] px-[14px] py-[11px] text-sm text-slate-900 placeholder:text-gray-400 focus:border-[#2f5fe0] focus:outline-none focus:ring-[3px] focus:ring-[#2f5fe0]/15"
259
- placeholder="Kinh nghiệm hiện tại, mục tiêu học tập..." />
260
- </div>
261
-
262
- <!-- Submit -->
263
- <button type="submit" :disabled="submitting"
264
- class="flex w-full items-center justify-center gap-2 rounded-[10px] bg-[#2f5fe0] px-5 py-[13px] text-[14.5px] font-semibold text-white transition-colors hover:bg-[#274ec2] disabled:opacity-60">
265
- <span v-if="submitting">Đang gửi...</span>
266
- <template v-else>
267
- Gửi đăng
268
- <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
269
- stroke-linecap="round" stroke-linejoin="round">
270
- <line x1="22" y1="2" x2="11" y2="13" />
271
- <polygon points="22 2 15 22 11 13 2 9 22 2" />
272
- </svg>
273
- </template>
274
- </button>
275
- </form>
276
-
277
- <!-- Success message -->
278
- <div v-if="submitSuccess"
279
- class="mb-5 mt-5 rounded-[10px] border border-green-200 bg-green-50 p-4 text-sm text-green-700">
280
- Đăng ký thành công! Chúng tôi sẽ liên hệ với bạn trong thời gian sớm nhất.
281
- </div>
282
- </section>
283
- </template>
1
+ <script setup lang="ts">
2
+ const props = withDefaults(defineProps<{
3
+ plans?: Array<{
4
+ id: string
5
+ name: string
6
+ level: string
7
+ duration: string
8
+ icon: string
9
+ price?: string
10
+ description?: string
11
+ }>
12
+ selectedPlanId?: string
13
+ }>(), {
14
+ plans: () => [],
15
+ })
16
+
17
+ const { addOpportunity } = useCrmApi()
18
+
19
+ // Form state
20
+ const selectedCourseId = ref(props.selectedPlanId || '')
21
+ const form = reactive({ name: '', phone: '', email: '', note: '' })
22
+ const studyMode = ref('online')
23
+ const errors = reactive({ name: '', phone: '', email: '', course: '' })
24
+ const touched = reactive({ name: false, phone: false, email: false, course: false })
25
+ const submitting = ref(false)
26
+ const submitSuccess = ref(false)
27
+
28
+ // Auto-select first plan if none selected
29
+ watch(() => props.plans, (list: any) => {
30
+ if (!selectedCourseId.value && list.length > 0) {
31
+ selectedCourseId.value = list[0].id
32
+ }
33
+ }, { immediate: true })
34
+
35
+ // Sync when selectedPlanId prop changes
36
+ watch(() => props.selectedPlanId, (id) => {
37
+ if (id) selectedCourseId.value = id
38
+ })
39
+
40
+ const selectedPlan = computed(() => props.plans.find((p) => p.id === selectedCourseId.value))
41
+
42
+ // Validation
43
+ function touch(field: keyof typeof touched) {
44
+ touched[field] = true
45
+ validateField(field)
46
+ }
47
+
48
+ function validateField(field: keyof typeof touched) {
49
+ if (field === 'name') {
50
+ if (!form.name) errors.name = 'Vui lòng nhập họ tên'
51
+ else if (form.name.length < 2) errors.name = 'Họ tên phải có ít nhất 2 ký tự'
52
+ else errors.name = ''
53
+ }
54
+ if (field === 'phone') {
55
+ if (!form.phone) errors.phone = 'Vui lòng nhập số điện thoại'
56
+ else if (!/^(0|\+84)\d{9,10}$/.test(form.phone.replace(/\s/g, ''))) errors.phone = 'Số điện thoại không hợp lệ'
57
+ else errors.phone = ''
58
+ }
59
+ if (field === 'email') {
60
+ if (form.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) errors.email = 'Email không hợp lệ'
61
+ else errors.email = ''
62
+ }
63
+ if (field === 'course') {
64
+ if (!selectedCourseId.value) errors.course = 'Vui lòng chọn khóa học'
65
+ else errors.course = ''
66
+ }
67
+ }
68
+
69
+ function formatLevel(level?: string) {
70
+ const labels: Record<string, string> = {
71
+ basic: 'Cơ bản',
72
+ advanced: 'Nâng cao',
73
+ expert: 'Chuyên gia',
74
+ }
75
+
76
+ return labels[level || ''] || level || ''
77
+ }
78
+
79
+ // Ghép level + duration, bỏ qua phần nào rỗng thay vì để dính dấu "·" trơ
80
+ function planMetaLine(plan: { level?: string; duration?: string }) {
81
+ return [formatLevel(plan.level), plan.duration].filter(Boolean).join(' · ')
82
+ }
83
+
84
+ function validateAll() {
85
+ ; (['name', 'phone', 'email', 'course'] as const).forEach((f) => {
86
+ touched[f] = true
87
+ validateField(f)
88
+ })
89
+ return !errors.name && !errors.phone && !errors.email && !errors.course
90
+ }
91
+
92
+ // Submit
93
+ async function handleSubmit() {
94
+ if (!validateAll()) return
95
+ submitting.value = true
96
+ submitSuccess.value = false
97
+
98
+ try {
99
+ const hostname = window.location.origin
100
+ const studyModeLabel =
101
+ studyMode.value === 'online'
102
+ ? 'Học trực tuyến (Online)'
103
+ : studyMode.value === 'offline'
104
+ ? 'Học tại lớp (Offline)'
105
+ : 'Đào tạo doanh nghiệp'
106
+
107
+ const levelLabel =
108
+ selectedPlan.value?.level === 'basic'
109
+ ? 'Cơ bản'
110
+ : selectedPlan.value?.level === 'advanced'
111
+ ? 'Nâng cao'
112
+ : selectedPlan.value?.level === 'expert'
113
+ ? 'Chuyên gia'
114
+ : selectedPlan.value?.level || ''
115
+
116
+
117
+ const description = [
118
+ `<strong>Khóa học:</strong> ${selectedPlan.value?.name || ''}`,
119
+ `<strong>Trình độ:</strong> ${levelLabel}`,
120
+ `<strong>Thời lượng:</strong> ${selectedPlan.value?.duration || ''}`,
121
+ `<strong>Hình thức học:</strong> ${studyModeLabel}`,
122
+ `<strong>Họ và tên:</strong> ${form.name}`,
123
+ `<strong>Số điện thoại:</strong> ${form.phone}`,
124
+ `<strong>Email:</strong> ${form.email || 'Không cung cấp'}`,
125
+ `<strong>Ghi chú:</strong> ${form.note || 'Không có'}`,
126
+ ].join('<br>')
127
+
128
+ await addOpportunity({
129
+ name: `Yêu cầu đăng ký khóa học ${selectedPlan.value?.name}`,
130
+ description: description,
131
+ referName: form.name,
132
+ referPhone: form.phone,
133
+ referEmail: form.email,
134
+ targetId: selectedPlan.value?.id || '',
135
+ targetType: 'PRODUCT',
136
+ targetUrl: `${hostname}${window.location.pathname}`,
137
+ extSource: 'WEBSITE',
138
+ })
139
+ submitSuccess.value = true
140
+
141
+ setTimeout(() => {
142
+ submitSuccess.value = false
143
+ }, 2000)
144
+ form.name = ''
145
+ form.phone = ''
146
+ form.email = ''
147
+ form.note = ''
148
+ Object.keys(touched).forEach((k) => (touched[k as keyof typeof touched] = false))
149
+ } catch (err) {
150
+ console.error('Error submitting registration:', err)
151
+ errors.name = 'Gửi đăng ký thất bại, vui lòng thử lại.'
152
+ touched.name = true
153
+ } finally {
154
+ submitting.value = false
155
+ }
156
+ }
157
+ </script>
158
+
159
+ <template>
160
+ <section class="rounded-2xl border border-[#e7e9f0] bg-white p-8">
161
+ <h2 class="mb-5 text-[17px] font-bold text-slate-900">Thông tin đăng ký</h2>
162
+
163
+ <form @submit.prevent="handleSubmit">
164
+ <!-- Course selection -->
165
+ <div class="mb-5">
166
+ <label class="mb-[10px] block text-[13px] font-semibold text-gray-700">
167
+ {{ plans.length === 1 ? 'Gói bạn đăng ký tư vấn' : 'Chọn khóa học' }}
168
+ </label>
169
+
170
+ <!-- Đã chọn sẵn 1 gói từ trang trước -> chỉ hiển thị thông tin, không cần chọn lại -->
171
+ <div v-if="plans.length === 1" class="flex flex-col gap-2 rounded-[10px] border border-[#2f5fe0] bg-[#eef3ff] px-4 py-[14px]">
172
+ <div class="flex items-center gap-3">
173
+ <span class="relative h-[18px] w-[18px] flex-none rounded-[10px] border-2 border-[#2f5fe0]">
174
+ <span class="absolute inset-[3px] rounded-[10px] bg-[#2f5fe0]" />
175
+ </span>
176
+ <span class="flex flex-col gap-[2px]">
177
+ <span class="text-sm font-semibold text-slate-900">{{ plans[0].name }}</span>
178
+ <span v-if="planMetaLine(plans[0])" class="text-[12.5px] text-gray-500">{{ planMetaLine(plans[0]) }}</span>
179
+ </span>
180
+ </div>
181
+ <p v-if="plans[0].price" class="pl-[30px] text-[12.5px] font-semibold text-[#2f5fe0]">{{ plans[0].price }}</p>
182
+ <p v-if="plans[0].description" class="pl-[30px] text-[12.5px] text-gray-500">{{ plans[0].description }}</p>
183
+ </div>
184
+
185
+ <div v-else class="flex flex-col gap-[10px]">
186
+ <label v-for="course in plans" :key="course.id"
187
+ class="relative flex cursor-pointer items-center gap-3 rounded-[10px] border px-4 py-[14px] transition-colors"
188
+ :class="selectedCourseId === course.id ? 'border-[#2f5fe0] bg-[#eef3ff]' : 'border-[#e2e5ee] bg-white'">
189
+ <input type="radio" name="course" class="absolute h-0 w-0 opacity-0" :value="course.id"
190
+ v-model="selectedCourseId" @change="touch('course')">
191
+ <span class="relative h-[18px] w-[18px] flex-none rounded-[10px] border-2 transition-colors"
192
+ :class="selectedCourseId === course.id ? 'border-[#2f5fe0]' : 'border-[#cbd2e1]'">
193
+ <span v-if="selectedCourseId === course.id" class="absolute inset-[3px] rounded-[10px] bg-[#2f5fe0]" />
194
+ </span>
195
+ <span class="flex flex-col gap-[2px]">
196
+ <span class="text-sm font-semibold text-slate-900">{{ course.name }}</span>
197
+ <span v-if="planMetaLine(course)" class="text-[12.5px] text-gray-500">{{ planMetaLine(course) }}</span>
198
+ </span>
199
+ </label>
200
+ </div>
201
+ <p v-if="touched.course && errors.course" class="mt-1 text-[12px] text-red-500">{{ errors.course }}</p>
202
+ </div>
203
+
204
+ <!-- Name & Phone -->
205
+ <div class="mb-5 grid grid-cols-1 gap-4 sm:grid-cols-2">
206
+ <div>
207
+ <label class="mb-[10px] block text-[13px] font-semibold text-gray-700" for="fullname">Họ và tên</label>
208
+ <input id="fullname" v-model="form.name" type="text"
209
+ class="w-full rounded-[10px] border px-[14px] py-[11px] text-sm text-slate-900 placeholder:text-gray-400 focus:outline-none focus:ring-[3px]"
210
+ :class="touched.name && errors.name ? 'border-red-400 focus:border-red-400 focus:ring-red-400/15' : 'border-[#e2e5ee] focus:border-[#2f5fe0] focus:ring-[#2f5fe0]/15'"
211
+ placeholder="Nguyễn Văn A" @blur="touch('name')">
212
+ <p v-if="touched.name && errors.name" class="mt-1 text-[12px] text-red-500">{{ errors.name }}</p>
213
+ </div>
214
+ <div>
215
+ <label class="mb-[10px] block text-[13px] font-semibold text-gray-700" for="phone">Số điện thoại</label>
216
+ <input id="phone" v-model="form.phone" type="tel"
217
+ class="w-full rounded-[10px] border px-[14px] py-[11px] text-sm text-slate-900 placeholder:text-gray-400 focus:outline-none focus:ring-[3px]"
218
+ :class="touched.phone && errors.phone ? 'border-red-400 focus:border-red-400 focus:ring-red-400/15' : 'border-[#e2e5ee] focus:border-[#2f5fe0] focus:ring-[#2f5fe0]/15'"
219
+ placeholder="0912 345 678" @blur="touch('phone')">
220
+ <p v-if="touched.phone && errors.phone" class="mt-1 text-[12px] text-red-500">{{ errors.phone }}</p>
221
+ </div>
222
+ </div>
223
+
224
+ <!-- Email -->
225
+ <div class="mb-5">
226
+ <label class="mb-[10px] block text-[13px] font-semibold text-gray-700" for="email">Email</label>
227
+ <input id="email" v-model="form.email" type="email"
228
+ class="w-full rounded-[10px] border px-[14px] py-[11px] text-sm text-slate-900 placeholder:text-gray-400 focus:outline-none focus:ring-[3px]"
229
+ :class="touched.email && errors.email ? 'border-red-400 focus:border-red-400 focus:ring-red-400/15' : 'border-[#e2e5ee] focus:border-[#2f5fe0] focus:ring-[#2f5fe0]/15'"
230
+ placeholder="example@congty.vn" @blur="touch('email')">
231
+ <p v-if="touched.email && errors.email" class="mt-1 text-[12px] text-red-500">{{ errors.email }}</p>
232
+ </div>
233
+
234
+ <!-- Study mode -->
235
+ <div class="mb-5">
236
+ <label class="mb-[10px] block text-[13px] font-semibold text-gray-700">Hình thức học</label>
237
+ <div class="flex flex-wrap gap-[10px]">
238
+ <button type="button" class="rounded-[10px] border px-4 py-[9px] text-[13.5px] font-medium transition-colors"
239
+ :class="studyMode === 'online' ? 'border-[#2f5fe0] bg-[#2f5fe0] text-white' : 'border-[#e2e5ee] bg-white text-gray-700'"
240
+ @click="studyMode = 'online'">
241
+ Học trực tuyến (Online)
242
+ </button>
243
+ <button type="button" class="rounded-[10px] border px-4 py-[9px] text-[13.5px] font-medium transition-colors"
244
+ :class="studyMode === 'offline' ? 'border-[#2f5fe0] bg-[#2f5fe0] text-white' : 'border-[#e2e5ee] bg-white text-gray-700'"
245
+ @click="studyMode = 'offline'">
246
+ Học tại lớp (Offline)
247
+ </button>
248
+ <button type="button" class="rounded-[10px] border px-4 py-[9px] text-[13.5px] font-medium transition-colors"
249
+ :class="studyMode === 'enterprise' ? 'border-[#2f5fe0] bg-[#2f5fe0] text-white' : 'border-[#e2e5ee] bg-white text-gray-700'"
250
+ @click="studyMode = 'enterprise'">
251
+ Đào tạo doanh nghiệp
252
+ </button>
253
+ </div>
254
+ </div>
255
+
256
+ <!-- Notes -->
257
+ <div class="mb-5">
258
+ <label class="mb-[10px] block text-[13px] font-semibold text-gray-700" for="note">Ghi chú (tùy chọn)</label>
259
+ <textarea id="note" v-model="form.note" rows="3"
260
+ class="w-full min-h-[84px] resize-y rounded-[10px] border border-[#e2e5ee] bg-[#fafafb] px-[14px] py-[11px] text-sm text-slate-900 placeholder:text-gray-400 focus:border-[#2f5fe0] focus:outline-none focus:ring-[3px] focus:ring-[#2f5fe0]/15"
261
+ placeholder="Kinh nghiệm hiện tại, mục tiêu học tập..." />
262
+ </div>
263
+
264
+ <!-- Submit -->
265
+ <button type="submit" :disabled="submitting"
266
+ class="flex w-full items-center justify-center gap-2 rounded-[10px] bg-[#2f5fe0] px-5 py-[13px] text-[14.5px] font-semibold text-white transition-colors hover:bg-[#274ec2] disabled:opacity-60">
267
+ <span v-if="submitting">Đang gửi...</span>
268
+ <template v-else>
269
+ Gửi đăng ký
270
+ <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
271
+ stroke-linecap="round" stroke-linejoin="round">
272
+ <line x1="22" y1="2" x2="11" y2="13" />
273
+ <polygon points="22 2 15 22 11 13 2 9 22 2" />
274
+ </svg>
275
+ </template>
276
+ </button>
277
+ </form>
278
+
279
+ <!-- Success message -->
280
+ <div v-if="submitSuccess"
281
+ class="mb-5 mt-5 rounded-[10px] border border-green-200 bg-green-50 p-4 text-sm text-green-700">
282
+ Đăng ký thành công! Chúng tôi sẽ liên hệ với bạn trong thời gian sớm nhất.
283
+ </div>
284
+ </section>
285
+ </template>