@vicket/create-support 1.1.1 → 1.1.2
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.
- package/bin/create-vicket-support.js +429 -389
- package/package.json +1 -1
- package/templates/next/src/app/api/vicket/[...path]/route.ts +2 -55
- package/templates/next/src/app/components/vicket/ReplyForm.tsx +154 -0
- package/templates/next/src/app/components/vicket/SupportContent.tsx +298 -0
- package/templates/next/src/app/components/vicket/TicketDialog.tsx +3 -3
- package/templates/next/src/app/support/page.tsx +27 -353
- package/templates/next/src/app/ticket/page.tsx +110 -325
- package/templates/next/src/app/vicket.css +1325 -1325
- package/templates/nuxt/app/assets/css/vicket.css +1325 -1325
- package/templates/nuxt/app/components/VicketReplyForm.vue +154 -0
- package/templates/nuxt/app/components/VicketSupportContent.vue +255 -0
- package/templates/nuxt/app/components/VicketTicketDialog.vue +2 -2
- package/templates/nuxt/app/pages/support.vue +7 -293
- package/templates/nuxt/app/pages/ticket.vue +36 -178
- package/templates/nuxt/server/api/vicket/[...path].ts +2 -85
- package/templates/sveltekit/src/lib/vicket/ReplyForm.svelte +134 -0
- package/templates/sveltekit/src/lib/vicket/SupportContent.svelte +263 -0
- package/templates/sveltekit/src/lib/vicket/TicketDialog.svelte +457 -459
- package/templates/sveltekit/src/lib/vicket.css +1325 -1325
- package/templates/sveltekit/src/routes/api/vicket/[...path]/+server.ts +2 -76
- package/templates/sveltekit/src/routes/support/+page.server.ts +13 -0
- package/templates/sveltekit/src/routes/support/+page.svelte +3 -312
- package/templates/sveltekit/src/routes/ticket/+page.server.ts +19 -0
- package/templates/sveltekit/src/routes/ticket/+page.svelte +13 -188
- package/templates-tailwind/next/src/app/api/vicket/[...path]/route.ts +6 -0
- package/templates-tailwind/next/src/app/support/page.tsx +33 -3
- package/templates-tailwind/next/src/app/ticket/page.tsx +249 -6
- package/templates-tailwind/next/src/components/vicket/reply-form.tsx +113 -0
- package/templates-tailwind/next/src/components/vicket/support-content.tsx +265 -0
- package/templates-tailwind/next/src/components/vicket/ticket-dialog.tsx +2 -2
- package/templates-tailwind/nuxt/app/components/VicketReplyForm.vue +169 -0
- package/templates-tailwind/nuxt/app/components/{VicketSupportPage.vue → VicketSupportContent.vue} +275 -317
- package/templates-tailwind/nuxt/app/components/VicketTicketDialog.vue +3 -0
- package/templates-tailwind/nuxt/app/pages/support.vue +10 -1
- package/templates-tailwind/nuxt/app/pages/ticket.vue +298 -1
- package/templates-tailwind/nuxt/server/api/vicket/[...path].ts +2 -0
- package/templates-tailwind/sveltekit/src/lib/vicket/ReplyForm.svelte +127 -0
- package/templates-tailwind/sveltekit/src/lib/vicket/{SupportPage.svelte → SupportContent.svelte} +9 -71
- package/templates-tailwind/sveltekit/src/lib/vicket/TicketDialog.svelte +405 -406
- package/templates-tailwind/sveltekit/src/routes/api/vicket/[...path]/+server.ts +3 -0
- package/templates-tailwind/sveltekit/src/routes/support/+page.server.ts +13 -0
- package/templates-tailwind/sveltekit/src/routes/support/+page.svelte +4 -2
- package/templates-tailwind/sveltekit/src/routes/ticket/+page.server.ts +19 -0
- package/templates-tailwind/sveltekit/src/routes/ticket/+page.svelte +292 -2
- package/templates/next/src/app/utils/vicket/api.ts +0 -149
- package/templates/next/src/app/utils/vicket/types.ts +0 -85
- package/templates/next/src/app/utils/vicket/utils.ts +0 -49
- package/templates/nuxt/app/composables/useVicket.ts +0 -274
- package/templates/sveltekit/src/lib/vicket/api.ts +0 -162
- package/templates/sveltekit/src/lib/vicket/types.ts +0 -87
- package/templates/sveltekit/src/lib/vicket/utils.ts +0 -55
- package/templates-tailwind/next/src/app/api/vicket/init/route.ts +0 -24
- package/templates-tailwind/next/src/app/api/vicket/messages/route.ts +0 -36
- package/templates-tailwind/next/src/app/api/vicket/thread/route.ts +0 -27
- package/templates-tailwind/next/src/app/api/vicket/tickets/route.ts +0 -37
- package/templates-tailwind/next/src/components/vicket/support-page.tsx +0 -359
- package/templates-tailwind/next/src/components/vicket/ticket-page.tsx +0 -425
- package/templates-tailwind/next/src/lib/vicket.ts +0 -257
- package/templates-tailwind/nuxt/app/components/VicketTicketPage.vue +0 -449
- package/templates-tailwind/nuxt/app/composables/use-vicket.ts +0 -249
- package/templates-tailwind/nuxt/server/api/vicket/init.get.ts +0 -22
- package/templates-tailwind/nuxt/server/api/vicket/messages.post.ts +0 -56
- package/templates-tailwind/nuxt/server/api/vicket/thread.get.ts +0 -26
- package/templates-tailwind/nuxt/server/api/vicket/tickets.post.ts +0 -53
- package/templates-tailwind/sveltekit/src/lib/vicket/TicketPage.svelte +0 -465
- package/templates-tailwind/sveltekit/src/lib/vicket/index.ts +0 -257
- package/templates-tailwind/sveltekit/src/routes/api/vicket/init/+server.ts +0 -22
- package/templates-tailwind/sveltekit/src/routes/api/vicket/messages/+server.ts +0 -40
- package/templates-tailwind/sveltekit/src/routes/api/vicket/thread/+server.ts +0 -25
- package/templates-tailwind/sveltekit/src/routes/api/vicket/tickets/+server.ts +0 -37
|
@@ -1,459 +1,457 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
let
|
|
13
|
-
let
|
|
14
|
-
let
|
|
15
|
-
let
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (!
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
>
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
<
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
>
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
>
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
>
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
<
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
>
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
>
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
{
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
>
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
{
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
</div>
|
|
459
|
-
{/if}
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn, createTicket, initialFormValues, type Template, type FormValues } from "vicket";
|
|
3
|
+
|
|
4
|
+
const MAX_FILES = 3;
|
|
5
|
+
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
|
|
6
|
+
|
|
7
|
+
let { open, onclose, templates }: { open: boolean; onclose: () => void; templates: Template[] } = $props();
|
|
8
|
+
|
|
9
|
+
/* -- Internal state --------------------------- */
|
|
10
|
+
let step = $state<"identify" | "details" | "success">("identify");
|
|
11
|
+
let selectedTemplateId = $state("");
|
|
12
|
+
let form = $state<FormValues>({ ...initialFormValues });
|
|
13
|
+
let isSubmitting = $state(false);
|
|
14
|
+
let dialogError = $state("");
|
|
15
|
+
let emailLimitReached = $state(false);
|
|
16
|
+
|
|
17
|
+
/* -- Derived ---------------------------------- */
|
|
18
|
+
let selectedTemplate = $derived(templates.find((t) => t.id === selectedTemplateId) || null);
|
|
19
|
+
let orderedQuestions = $derived(
|
|
20
|
+
[...(selectedTemplate?.questions || [])].sort((a, b) => a.order - b.order),
|
|
21
|
+
);
|
|
22
|
+
let canContinue = $derived(form.email.trim().length > 0);
|
|
23
|
+
|
|
24
|
+
/* -- Sync default template when templates change -- */
|
|
25
|
+
$effect(() => {
|
|
26
|
+
if (templates.length > 0 && !selectedTemplateId) {
|
|
27
|
+
selectedTemplateId = templates[0].id;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/* -- Functions -------------------------------- */
|
|
32
|
+
function resetAndClose() {
|
|
33
|
+
step = "identify";
|
|
34
|
+
form = { ...initialFormValues };
|
|
35
|
+
dialogError = "";
|
|
36
|
+
selectedTemplateId = templates.length > 0 ? templates[0].id : "";
|
|
37
|
+
onclose();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function updateAnswer(questionId: string, value: unknown) {
|
|
41
|
+
form = { ...form, answers: { ...form.answers, [questionId]: value } };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let fileInputRefs: Record<string, HTMLInputElement | null> = $state({});
|
|
45
|
+
|
|
46
|
+
function getFileArray(questionId: string): File[] {
|
|
47
|
+
const val = form.answers[questionId];
|
|
48
|
+
if (Array.isArray(val)) return val as File[];
|
|
49
|
+
if (val instanceof File) return [val];
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const ALLOWED_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
54
|
+
|
|
55
|
+
function handleFileAdd(questionId: string, event: Event) {
|
|
56
|
+
const input = event.currentTarget as HTMLInputElement;
|
|
57
|
+
const newFiles = input.files;
|
|
58
|
+
if (!newFiles) return;
|
|
59
|
+
const current = getFileArray(questionId);
|
|
60
|
+
const remaining = MAX_FILES - current.length;
|
|
61
|
+
if (remaining <= 0) return;
|
|
62
|
+
const accepted: File[] = [];
|
|
63
|
+
for (let i = 0; i < Math.min(newFiles.length, remaining); i++) {
|
|
64
|
+
if (!ALLOWED_TYPES.includes(newFiles[i].type)) continue;
|
|
65
|
+
if (newFiles[i].size > MAX_FILE_SIZE) continue;
|
|
66
|
+
accepted.push(newFiles[i]);
|
|
67
|
+
}
|
|
68
|
+
if (accepted.length > 0) updateAnswer(questionId, [...current, ...accepted]);
|
|
69
|
+
input.value = "";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function removeFileAt(questionId: string, index: number) {
|
|
73
|
+
const current = getFileArray(questionId);
|
|
74
|
+
const next = current.filter((_, i) => i !== index);
|
|
75
|
+
updateAnswer(questionId, next.length > 0 ? next : null);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function toggleCheckboxValue(questionId: string, value: string, checked: boolean) {
|
|
79
|
+
const current = Array.isArray(form.answers[questionId])
|
|
80
|
+
? (form.answers[questionId] as string[])
|
|
81
|
+
: [];
|
|
82
|
+
const next = checked
|
|
83
|
+
? [...new Set([...current, value])]
|
|
84
|
+
: current.filter((item) => item !== value);
|
|
85
|
+
updateAnswer(questionId, next);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function validateRequired(): string {
|
|
89
|
+
if (!selectedTemplate) return "Please select a template.";
|
|
90
|
+
if (!form.email.trim()) return "Email is required.";
|
|
91
|
+
if (!form.title.trim()) return "Subject is required.";
|
|
92
|
+
|
|
93
|
+
for (const question of orderedQuestions) {
|
|
94
|
+
if (!question.required) continue;
|
|
95
|
+
const value = form.answers[question.id];
|
|
96
|
+
|
|
97
|
+
if (question.type === "CHECKBOX") {
|
|
98
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
99
|
+
return `"${question.label}" is required.`;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (question.type === "FILE") {
|
|
103
|
+
const hasFiles = Array.isArray(value) ? value.length > 0 : value instanceof File;
|
|
104
|
+
if (!hasFiles) return `"${question.label}" is required.`;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (value === null || value === undefined || String(value).trim() === "") {
|
|
108
|
+
return `"${question.label}" is required.`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function handleSubmit(event: SubmitEvent) {
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
dialogError = "";
|
|
117
|
+
|
|
118
|
+
const validationError = validateRequired();
|
|
119
|
+
if (validationError) {
|
|
120
|
+
dialogError = validationError;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!selectedTemplate) {
|
|
124
|
+
dialogError = "Template is required.";
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
isSubmitting = true;
|
|
129
|
+
try {
|
|
130
|
+
const fileQuestionIds = orderedQuestions
|
|
131
|
+
.filter((q) => {
|
|
132
|
+
const val = form.answers[q.id];
|
|
133
|
+
return q.type === "FILE" && (val instanceof File || (Array.isArray(val) && val.length > 0));
|
|
134
|
+
})
|
|
135
|
+
.map((q) => q.id);
|
|
136
|
+
|
|
137
|
+
const result = await createTicket({
|
|
138
|
+
email: form.email.trim(),
|
|
139
|
+
title: form.title.trim(),
|
|
140
|
+
templateId: selectedTemplate.id,
|
|
141
|
+
answers: { ...form.answers },
|
|
142
|
+
hasFiles: fileQuestionIds.length > 0,
|
|
143
|
+
fileQuestionIds,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
emailLimitReached = result.emailLimitReached ?? false;
|
|
147
|
+
step = "success";
|
|
148
|
+
} catch (submitError) {
|
|
149
|
+
dialogError = submitError instanceof Error ? submitError.message : "Unexpected error.";
|
|
150
|
+
} finally {
|
|
151
|
+
isSubmitting = false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
</script>
|
|
155
|
+
|
|
156
|
+
{#if open}
|
|
157
|
+
<div class="vk-dialog-overlay" onclick={resetAndClose}>
|
|
158
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
159
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
160
|
+
<div class="vk-dialog vk-slide-up" onclick={(e: MouseEvent) => e.stopPropagation()}>
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
onclick={resetAndClose}
|
|
164
|
+
class="vk-dialog-close"
|
|
165
|
+
aria-label="Close"
|
|
166
|
+
>
|
|
167
|
+
✕
|
|
168
|
+
</button>
|
|
169
|
+
|
|
170
|
+
<!-- Success state -->
|
|
171
|
+
{#if step === "success"}
|
|
172
|
+
<div class="vk-dialog-body vk-slide-up">
|
|
173
|
+
<div class="vk-success-icon">
|
|
174
|
+
<span>✓</span>
|
|
175
|
+
</div>
|
|
176
|
+
<div class="vk-empty-state">
|
|
177
|
+
<h2 class="vk-dialog-title">Ticket submitted!</h2>
|
|
178
|
+
{#if emailLimitReached}
|
|
179
|
+
<p class="vk-empty-text">
|
|
180
|
+
Your ticket was created, but the daily email limit for this service has been reached. No confirmation email was sent. Please consider using a self-hosted email delivery setup for unlimited emails.
|
|
181
|
+
</p>
|
|
182
|
+
{:else}
|
|
183
|
+
<p class="vk-empty-text">
|
|
184
|
+
Check your email for a secure link to follow your ticket.
|
|
185
|
+
</p>
|
|
186
|
+
{/if}
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
class="vk-button pill"
|
|
190
|
+
onclick={resetAndClose}
|
|
191
|
+
>
|
|
192
|
+
Close
|
|
193
|
+
</button>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
{/if}
|
|
197
|
+
|
|
198
|
+
<!-- Step 1: Identify -->
|
|
199
|
+
{#if step === "identify"}
|
|
200
|
+
<div class="vk-dialog-body">
|
|
201
|
+
<h2 class="vk-dialog-title">Submit a request</h2>
|
|
202
|
+
<p class="vk-dialog-subtitle">
|
|
203
|
+
We'll get back to you as soon as possible.
|
|
204
|
+
</p>
|
|
205
|
+
|
|
206
|
+
<div class="vk-stack">
|
|
207
|
+
{#if dialogError}
|
|
208
|
+
<div class={cn("vk-alert vk-slide-up", "error")} role="alert">
|
|
209
|
+
<span>⚠</span>
|
|
210
|
+
<span>{dialogError}</span>
|
|
211
|
+
<button
|
|
212
|
+
type="button"
|
|
213
|
+
onclick={() => (dialogError = "")}
|
|
214
|
+
class="vk-alert-dismiss"
|
|
215
|
+
aria-label="Dismiss"
|
|
216
|
+
>
|
|
217
|
+
✕
|
|
218
|
+
</button>
|
|
219
|
+
</div>
|
|
220
|
+
{/if}
|
|
221
|
+
|
|
222
|
+
<!-- Email -->
|
|
223
|
+
<div class="vk-field">
|
|
224
|
+
<label class="vk-label required" for="vk-dialog-email">
|
|
225
|
+
Email
|
|
226
|
+
</label>
|
|
227
|
+
<div class="vk-input-icon-wrap">
|
|
228
|
+
<span class="vk-input-icon">✉</span>
|
|
229
|
+
<input
|
|
230
|
+
id="vk-dialog-email"
|
|
231
|
+
class="vk-input"
|
|
232
|
+
type="email"
|
|
233
|
+
placeholder="you@example.com"
|
|
234
|
+
bind:value={form.email}
|
|
235
|
+
required
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
|
+
<p class="vk-hint">We'll contact you at this address.</p>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<!-- Template selector -->
|
|
242
|
+
{#if templates.length > 1}
|
|
243
|
+
<div class="vk-field">
|
|
244
|
+
<p class="vk-label">What can we help you with?</p>
|
|
245
|
+
<div class="vk-grid">
|
|
246
|
+
{#each templates as template (template.id)}
|
|
247
|
+
<button
|
|
248
|
+
type="button"
|
|
249
|
+
class={cn(
|
|
250
|
+
"vk-radio-card",
|
|
251
|
+
selectedTemplateId === template.id && "active",
|
|
252
|
+
)}
|
|
253
|
+
onclick={() => (selectedTemplateId = template.id)}
|
|
254
|
+
>
|
|
255
|
+
<span class="vk-radio-dot">
|
|
256
|
+
<span class="vk-radio-dot-inner"></span>
|
|
257
|
+
</span>
|
|
258
|
+
<div>
|
|
259
|
+
<span class="vk-radio-name">{template.name}</span>
|
|
260
|
+
{#if template.description}
|
|
261
|
+
<span class="vk-radio-description">
|
|
262
|
+
{template.description}
|
|
263
|
+
</span>
|
|
264
|
+
{/if}
|
|
265
|
+
</div>
|
|
266
|
+
</button>
|
|
267
|
+
{/each}
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
{/if}
|
|
271
|
+
|
|
272
|
+
<!-- Continue -->
|
|
273
|
+
<button
|
|
274
|
+
type="button"
|
|
275
|
+
class="vk-button primary full"
|
|
276
|
+
disabled={!canContinue}
|
|
277
|
+
onclick={() => {
|
|
278
|
+
dialogError = "";
|
|
279
|
+
step = "details";
|
|
280
|
+
}}
|
|
281
|
+
>
|
|
282
|
+
Continue
|
|
283
|
+
</button>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
{/if}
|
|
287
|
+
|
|
288
|
+
<!-- Step 2: Details -->
|
|
289
|
+
{#if step === "details"}
|
|
290
|
+
<div class="vk-dialog-body">
|
|
291
|
+
<button
|
|
292
|
+
type="button"
|
|
293
|
+
class="vk-button ghost"
|
|
294
|
+
onclick={() => {
|
|
295
|
+
dialogError = "";
|
|
296
|
+
step = "identify";
|
|
297
|
+
}}
|
|
298
|
+
>
|
|
299
|
+
← Back
|
|
300
|
+
</button>
|
|
301
|
+
|
|
302
|
+
<h2 class="vk-dialog-title">
|
|
303
|
+
{selectedTemplate?.name || "Ticket details"}
|
|
304
|
+
</h2>
|
|
305
|
+
|
|
306
|
+
<form class="vk-stack" onsubmit={handleSubmit}>
|
|
307
|
+
{#if dialogError}
|
|
308
|
+
<div class={cn("vk-alert vk-slide-up", "error")} role="alert">
|
|
309
|
+
<span>⚠</span>
|
|
310
|
+
<span>{dialogError}</span>
|
|
311
|
+
<button
|
|
312
|
+
type="button"
|
|
313
|
+
onclick={() => (dialogError = "")}
|
|
314
|
+
class="vk-alert-dismiss"
|
|
315
|
+
aria-label="Dismiss"
|
|
316
|
+
>
|
|
317
|
+
✕
|
|
318
|
+
</button>
|
|
319
|
+
</div>
|
|
320
|
+
{/if}
|
|
321
|
+
|
|
322
|
+
<!-- Subject -->
|
|
323
|
+
<div class="vk-field">
|
|
324
|
+
<label class="vk-label required" for="vk-dialog-subject">
|
|
325
|
+
Subject
|
|
326
|
+
</label>
|
|
327
|
+
<input
|
|
328
|
+
id="vk-dialog-subject"
|
|
329
|
+
class="vk-input"
|
|
330
|
+
type="text"
|
|
331
|
+
placeholder="Brief description of your issue"
|
|
332
|
+
bind:value={form.title}
|
|
333
|
+
required
|
|
334
|
+
/>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
<!-- Dynamic questions -->
|
|
338
|
+
{#each orderedQuestions as question (question.id)}
|
|
339
|
+
<div class="vk-field">
|
|
340
|
+
<label
|
|
341
|
+
class={cn("vk-label", question.required && "required")}
|
|
342
|
+
>
|
|
343
|
+
{question.label}
|
|
344
|
+
</label>
|
|
345
|
+
|
|
346
|
+
{#if question.type === "TEXT"}
|
|
347
|
+
<input
|
|
348
|
+
class="vk-input"
|
|
349
|
+
type="text"
|
|
350
|
+
value={String(form.answers[question.id] || "")}
|
|
351
|
+
oninput={(e: Event) => updateAnswer(question.id, (e.currentTarget as HTMLInputElement).value)}
|
|
352
|
+
/>
|
|
353
|
+
{:else if question.type === "TEXTAREA"}
|
|
354
|
+
<textarea
|
|
355
|
+
class="vk-textarea"
|
|
356
|
+
value={String(form.answers[question.id] || "")}
|
|
357
|
+
oninput={(e: Event) => updateAnswer(question.id, (e.currentTarget as HTMLTextAreaElement).value)}
|
|
358
|
+
></textarea>
|
|
359
|
+
{:else if question.type === "DATE"}
|
|
360
|
+
<input
|
|
361
|
+
class="vk-input"
|
|
362
|
+
type="date"
|
|
363
|
+
value={String(form.answers[question.id] || "")}
|
|
364
|
+
oninput={(e: Event) => updateAnswer(question.id, (e.currentTarget as HTMLInputElement).value)}
|
|
365
|
+
/>
|
|
366
|
+
{:else if question.type === "SELECT"}
|
|
367
|
+
<select
|
|
368
|
+
class="vk-select"
|
|
369
|
+
value={String(form.answers[question.id] || "")}
|
|
370
|
+
onchange={(e: Event) => updateAnswer(question.id, (e.currentTarget as HTMLSelectElement).value)}
|
|
371
|
+
>
|
|
372
|
+
<option value="">Select an option</option>
|
|
373
|
+
{#each question.options || [] as option (option.id)}
|
|
374
|
+
<option value={option.value}>{option.label}</option>
|
|
375
|
+
{/each}
|
|
376
|
+
</select>
|
|
377
|
+
{:else if question.type === "CHECKBOX"}
|
|
378
|
+
<div class="vk-checkbox-list">
|
|
379
|
+
{#each question.options || [] as option (option.id)}
|
|
380
|
+
<label class="vk-checkbox-item">
|
|
381
|
+
<input
|
|
382
|
+
type="checkbox"
|
|
383
|
+
checked={Array.isArray(form.answers[question.id]) &&
|
|
384
|
+
(form.answers[question.id] as string[]).includes(option.value)}
|
|
385
|
+
onchange={(e: Event) =>
|
|
386
|
+
toggleCheckboxValue(
|
|
387
|
+
question.id,
|
|
388
|
+
option.value,
|
|
389
|
+
(e.currentTarget as HTMLInputElement).checked,
|
|
390
|
+
)}
|
|
391
|
+
/>
|
|
392
|
+
<span>{option.label}</span>
|
|
393
|
+
</label>
|
|
394
|
+
{/each}
|
|
395
|
+
</div>
|
|
396
|
+
{:else if question.type === "FILE"}
|
|
397
|
+
{#if getFileArray(question.id).length > 0}
|
|
398
|
+
<div class="vk-image-grid">
|
|
399
|
+
{#each getFileArray(question.id) as file, idx (`${file.name}-${idx}`)}
|
|
400
|
+
<div class="vk-image-thumb">
|
|
401
|
+
<img src={URL.createObjectURL(file)} alt={file.name} />
|
|
402
|
+
<button
|
|
403
|
+
type="button"
|
|
404
|
+
class="vk-image-thumb-remove"
|
|
405
|
+
onclick={() => removeFileAt(question.id, idx)}
|
|
406
|
+
aria-label="Remove"
|
|
407
|
+
>
|
|
408
|
+
✕
|
|
409
|
+
</button>
|
|
410
|
+
<span class="vk-image-thumb-name">{file.name}</span>
|
|
411
|
+
</div>
|
|
412
|
+
{/each}
|
|
413
|
+
</div>
|
|
414
|
+
{/if}
|
|
415
|
+
{#if getFileArray(question.id).length < MAX_FILES}
|
|
416
|
+
<input
|
|
417
|
+
bind:this={fileInputRefs[question.id]}
|
|
418
|
+
type="file"
|
|
419
|
+
accept="image/jpeg,image/png,image/gif,image/webp"
|
|
420
|
+
multiple
|
|
421
|
+
style="display: none"
|
|
422
|
+
onchange={(e: Event) => handleFileAdd(question.id, e)}
|
|
423
|
+
/>
|
|
424
|
+
<button
|
|
425
|
+
type="button"
|
|
426
|
+
class="vk-add-image-btn"
|
|
427
|
+
onclick={() => fileInputRefs[question.id]?.click()}
|
|
428
|
+
>
|
|
429
|
+
📷
|
|
430
|
+
{getFileArray(question.id).length === 0
|
|
431
|
+
? `Add images (up to ${MAX_FILES})`
|
|
432
|
+
: `Add more (${MAX_FILES - getFileArray(question.id).length} remaining)`}
|
|
433
|
+
</button>
|
|
434
|
+
{/if}
|
|
435
|
+
{/if}
|
|
436
|
+
</div>
|
|
437
|
+
{/each}
|
|
438
|
+
|
|
439
|
+
<!-- Actions -->
|
|
440
|
+
<button
|
|
441
|
+
class="vk-button primary full"
|
|
442
|
+
type="submit"
|
|
443
|
+
disabled={isSubmitting}
|
|
444
|
+
>
|
|
445
|
+
{#if isSubmitting}
|
|
446
|
+
<span class="vk-spinner"></span>
|
|
447
|
+
Submitting...
|
|
448
|
+
{:else}
|
|
449
|
+
Submit
|
|
450
|
+
{/if}
|
|
451
|
+
</button>
|
|
452
|
+
</form>
|
|
453
|
+
</div>
|
|
454
|
+
{/if}
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
{/if}
|