@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,406 +1,405 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
let
|
|
17
|
-
let
|
|
18
|
-
let
|
|
19
|
-
let
|
|
20
|
-
let
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
let
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (!
|
|
73
|
-
if (!form.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
<!-- svelte-ignore
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
<!-- svelte-ignore
|
|
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
|
-
</span>
|
|
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
|
-
</span>
|
|
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
|
-
const
|
|
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
|
-
{/if}
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn, createTicket, initialFormValues, type Template, type FormValues } from "vicket";
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
open,
|
|
6
|
+
onclose,
|
|
7
|
+
templates,
|
|
8
|
+
}: {
|
|
9
|
+
open: boolean;
|
|
10
|
+
onclose: () => void;
|
|
11
|
+
templates: Template[];
|
|
12
|
+
} = $props();
|
|
13
|
+
|
|
14
|
+
/* -- Internal state -- */
|
|
15
|
+
let step = $state<"identify" | "details" | "success">("identify");
|
|
16
|
+
let selectedTemplateId = $state("");
|
|
17
|
+
let form = $state<FormValues>({ ...initialFormValues });
|
|
18
|
+
let isSubmitting = $state(false);
|
|
19
|
+
let dialogError = $state("");
|
|
20
|
+
let emailLimitReached = $state(false);
|
|
21
|
+
|
|
22
|
+
/* -- Derived -- */
|
|
23
|
+
let selectedTemplate = $derived(templates.find((t) => t.id === selectedTemplateId) || null);
|
|
24
|
+
let orderedQuestions = $derived(
|
|
25
|
+
[...(selectedTemplate?.questions || [])].sort((a, b) => a.order - b.order),
|
|
26
|
+
);
|
|
27
|
+
let canContinue = $derived(form.email.trim().length > 0);
|
|
28
|
+
|
|
29
|
+
/* -- Sync selectedTemplateId when templates changes -- */
|
|
30
|
+
$effect(() => {
|
|
31
|
+
if (templates.length > 0 && !selectedTemplateId) {
|
|
32
|
+
selectedTemplateId = templates[0].id;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/* -- Reset when dialog opens -- */
|
|
37
|
+
$effect(() => {
|
|
38
|
+
if (open) {
|
|
39
|
+
step = "identify";
|
|
40
|
+
form = { ...initialFormValues };
|
|
41
|
+
dialogError = "";
|
|
42
|
+
emailLimitReached = false;
|
|
43
|
+
selectedTemplateId = templates.length > 0 ? templates[0].id : "";
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/* -- Functions -- */
|
|
48
|
+
function resetAndClose() {
|
|
49
|
+
step = "identify";
|
|
50
|
+
form = { ...initialFormValues };
|
|
51
|
+
dialogError = "";
|
|
52
|
+
selectedTemplateId = templates.length > 0 ? templates[0].id : "";
|
|
53
|
+
onclose();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function updateAnswer(questionId: string, value: unknown) {
|
|
57
|
+
form = { ...form, answers: { ...form.answers, [questionId]: value } };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function toggleCheckboxValue(questionId: string, value: string, checked: boolean) {
|
|
61
|
+
const current = Array.isArray(form.answers[questionId])
|
|
62
|
+
? (form.answers[questionId] as string[])
|
|
63
|
+
: [];
|
|
64
|
+
const next = checked
|
|
65
|
+
? [...new Set([...current, value])]
|
|
66
|
+
: current.filter((item) => item !== value);
|
|
67
|
+
updateAnswer(questionId, next);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validateRequired(): string {
|
|
71
|
+
if (!selectedTemplate) return "Please select a template.";
|
|
72
|
+
if (!form.email.trim()) return "Email is required.";
|
|
73
|
+
if (!form.title.trim()) return "Subject is required.";
|
|
74
|
+
|
|
75
|
+
for (const question of orderedQuestions) {
|
|
76
|
+
if (!question.required) continue;
|
|
77
|
+
const value = form.answers[question.id];
|
|
78
|
+
|
|
79
|
+
if (question.type === "CHECKBOX") {
|
|
80
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
81
|
+
return `"${question.label}" is required.`;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (question.type === "FILE") {
|
|
85
|
+
if (!(value instanceof File)) return `"${question.label}" is required.`;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (value === null || value === undefined || String(value).trim() === "") {
|
|
89
|
+
return `"${question.label}" is required.`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function handleSubmit(event: SubmitEvent) {
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
dialogError = "";
|
|
98
|
+
|
|
99
|
+
const validationError = validateRequired();
|
|
100
|
+
if (validationError) {
|
|
101
|
+
dialogError = validationError;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!selectedTemplate) {
|
|
105
|
+
dialogError = "Template is required.";
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
isSubmitting = true;
|
|
110
|
+
try {
|
|
111
|
+
const fileQuestionIds = orderedQuestions
|
|
112
|
+
.filter((q) => q.type === "FILE" && form.answers[q.id] instanceof File)
|
|
113
|
+
.map((q) => q.id);
|
|
114
|
+
|
|
115
|
+
const result = await createTicket({
|
|
116
|
+
email: form.email.trim(),
|
|
117
|
+
title: form.title.trim(),
|
|
118
|
+
templateId: selectedTemplate.id,
|
|
119
|
+
answers: { ...form.answers },
|
|
120
|
+
hasFiles: fileQuestionIds.length > 0,
|
|
121
|
+
fileQuestionIds,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
emailLimitReached = result.emailLimitReached ?? false;
|
|
125
|
+
step = "success";
|
|
126
|
+
} catch (submitError) {
|
|
127
|
+
dialogError = submitError instanceof Error ? submitError.message : "Unexpected error.";
|
|
128
|
+
} finally {
|
|
129
|
+
isSubmitting = false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
{#if open}
|
|
135
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
136
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
137
|
+
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm" onclick={resetAndClose}>
|
|
138
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
139
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
140
|
+
<div class="relative mx-4 w-full max-w-lg overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-xl" onclick={(e) => e.stopPropagation()}>
|
|
141
|
+
<!-- Close button -->
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
onclick={resetAndClose}
|
|
145
|
+
class="absolute right-4 top-4 z-10 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border-none bg-transparent text-slate-500 transition-colors hover:bg-slate-50 hover:text-slate-900"
|
|
146
|
+
aria-label="Close"
|
|
147
|
+
>
|
|
148
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
|
|
149
|
+
</button>
|
|
150
|
+
|
|
151
|
+
{#if step === "success"}
|
|
152
|
+
<div class="px-6 py-12 text-center">
|
|
153
|
+
<div class="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-full bg-green-50 text-green-600">
|
|
154
|
+
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
|
|
155
|
+
</div>
|
|
156
|
+
<h2 class="m-0 text-xl font-bold text-slate-900">Ticket submitted!</h2>
|
|
157
|
+
{#if emailLimitReached}
|
|
158
|
+
<p class="mt-2 text-sm text-slate-500">
|
|
159
|
+
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.
|
|
160
|
+
</p>
|
|
161
|
+
{:else}
|
|
162
|
+
<p class="mt-2 text-sm text-slate-500">
|
|
163
|
+
Check your email for a secure link to follow your ticket.
|
|
164
|
+
</p>
|
|
165
|
+
{/if}
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
class="mt-6 inline-flex items-center gap-2 !rounded-full border border-slate-200 bg-white !px-7 !py-3 text-sm font-semibold text-slate-900 cursor-pointer transition-all hover:border-slate-300 hover:bg-slate-50"
|
|
169
|
+
onclick={resetAndClose}
|
|
170
|
+
>
|
|
171
|
+
Close
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
{:else if step === "identify"}
|
|
175
|
+
<div class="px-6 py-6">
|
|
176
|
+
<h2 class="m-0 text-lg font-bold text-slate-900">Submit a request</h2>
|
|
177
|
+
<p class="mt-1 text-sm text-slate-500">We'll get back to you as soon as possible.</p>
|
|
178
|
+
<div class="mt-5 space-y-5">
|
|
179
|
+
{#if dialogError}
|
|
180
|
+
<div class={cn("flex items-start gap-3 rounded-xl border p-4 text-sm", "border-red-200 bg-red-50 text-red-900")} role="alert">
|
|
181
|
+
<span class="mt-0.5 shrink-0">
|
|
182
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" /><path d="M12 9v4" /><path d="M12 17h.01" /></svg>
|
|
183
|
+
</span>
|
|
184
|
+
<span class="flex-1">{dialogError}</span>
|
|
185
|
+
<button type="button" onclick={() => (dialogError = "")} class="shrink-0 cursor-pointer border-none bg-transparent p-0 opacity-50 transition-opacity hover:opacity-100" aria-label="Dismiss">
|
|
186
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
{/if}
|
|
190
|
+
|
|
191
|
+
<!-- Email -->
|
|
192
|
+
<div class="space-y-1.5">
|
|
193
|
+
<label class="text-sm font-semibold text-slate-900" for="vk-dialog-email">Email<span class="text-red-600"> *</span></label>
|
|
194
|
+
<div class="relative">
|
|
195
|
+
<span class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-slate-500">
|
|
196
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="16" x="2" y="4" rx="2" /><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" /></svg>
|
|
197
|
+
</span>
|
|
198
|
+
<input
|
|
199
|
+
id="vk-dialog-email"
|
|
200
|
+
class="w-full rounded-lg border border-slate-200 bg-slate-50 py-2.5 pl-10 pr-3 text-sm text-slate-900 transition-all duration-150 placeholder:text-slate-500/60 focus:border-blue-600 focus:outline-none focus:ring-3 focus:ring-blue-600/10"
|
|
201
|
+
type="email"
|
|
202
|
+
placeholder="you@example.com"
|
|
203
|
+
bind:value={form.email}
|
|
204
|
+
required
|
|
205
|
+
/>
|
|
206
|
+
</div>
|
|
207
|
+
<p class="text-xs text-slate-500">We'll contact you at this address.</p>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<!-- Template selector -->
|
|
211
|
+
{#if templates.length > 1}
|
|
212
|
+
<div class="space-y-2">
|
|
213
|
+
<p class="m-0 text-sm font-semibold text-slate-900">What can we help you with?</p>
|
|
214
|
+
<div class="grid gap-2">
|
|
215
|
+
{#each templates as template (template.id)}
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
class={cn(
|
|
219
|
+
"flex w-full cursor-pointer items-start gap-3 rounded-xl border border-slate-200 bg-slate-50 p-3.5 text-left transition-all duration-150 hover:border-slate-300",
|
|
220
|
+
selectedTemplateId === template.id && "!border-blue-600 !bg-blue-50 ring-3 ring-blue-600/12",
|
|
221
|
+
)}
|
|
222
|
+
onclick={() => (selectedTemplateId = template.id)}
|
|
223
|
+
>
|
|
224
|
+
<span class={cn(
|
|
225
|
+
"mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border-2 transition-colors",
|
|
226
|
+
selectedTemplateId === template.id
|
|
227
|
+
? "border-blue-600 bg-blue-600"
|
|
228
|
+
: "border-slate-300 bg-transparent",
|
|
229
|
+
)}>
|
|
230
|
+
{#if selectedTemplateId === template.id}
|
|
231
|
+
<span class="block h-1.5 w-1.5 rounded-full bg-white"></span>
|
|
232
|
+
{/if}
|
|
233
|
+
</span>
|
|
234
|
+
<div>
|
|
235
|
+
<span class="block text-sm font-semibold text-slate-900">{template.name}</span>
|
|
236
|
+
{#if template.description}
|
|
237
|
+
<span class="mt-0.5 block text-xs text-slate-500">{template.description}</span>
|
|
238
|
+
{/if}
|
|
239
|
+
</div>
|
|
240
|
+
</button>
|
|
241
|
+
{/each}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
{/if}
|
|
245
|
+
|
|
246
|
+
<!-- Continue -->
|
|
247
|
+
<button
|
|
248
|
+
type="button"
|
|
249
|
+
class="inline-flex w-full items-center justify-center gap-2 rounded-lg border-none bg-blue-600 px-5 py-2.5 text-sm font-semibold text-white cursor-pointer transition-all hover:bg-blue-700 hover:-translate-y-px hover:shadow-lg active:translate-y-0 disabled:opacity-60 disabled:cursor-not-allowed"
|
|
250
|
+
disabled={!canContinue}
|
|
251
|
+
onclick={() => { dialogError = ""; step = "details"; }}
|
|
252
|
+
>
|
|
253
|
+
Continue
|
|
254
|
+
</button>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
{:else if step === "details"}
|
|
258
|
+
<div class="px-6 py-6">
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
class="-ml-2 -mt-1 mb-3 inline-flex cursor-pointer items-center gap-1 rounded-lg border-none bg-transparent px-2 py-1 text-sm font-medium text-slate-500 transition-colors hover:bg-slate-50 hover:text-slate-900"
|
|
262
|
+
onclick={() => { dialogError = ""; step = "identify"; }}
|
|
263
|
+
>
|
|
264
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg>
|
|
265
|
+
Back
|
|
266
|
+
</button>
|
|
267
|
+
|
|
268
|
+
<h2 class="m-0 text-lg font-bold text-slate-900">
|
|
269
|
+
{selectedTemplate?.name || "Ticket details"}
|
|
270
|
+
</h2>
|
|
271
|
+
|
|
272
|
+
<form class="mt-5 space-y-4" onsubmit={handleSubmit}>
|
|
273
|
+
{#if dialogError}
|
|
274
|
+
<div class={cn("flex items-start gap-3 rounded-xl border p-4 text-sm", "border-red-200 bg-red-50 text-red-900")} role="alert">
|
|
275
|
+
<span class="mt-0.5 shrink-0">
|
|
276
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" /><path d="M12 9v4" /><path d="M12 17h.01" /></svg>
|
|
277
|
+
</span>
|
|
278
|
+
<span class="flex-1">{dialogError}</span>
|
|
279
|
+
<button type="button" onclick={() => (dialogError = "")} class="shrink-0 cursor-pointer border-none bg-transparent p-0 opacity-50 transition-opacity hover:opacity-100" aria-label="Dismiss">
|
|
280
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
|
|
281
|
+
</button>
|
|
282
|
+
</div>
|
|
283
|
+
{/if}
|
|
284
|
+
|
|
285
|
+
<!-- Subject -->
|
|
286
|
+
<div class="space-y-1.5">
|
|
287
|
+
<label class="text-sm font-semibold text-slate-900" for="vk-dialog-subject">Subject<span class="text-red-600"> *</span></label>
|
|
288
|
+
<input
|
|
289
|
+
id="vk-dialog-subject"
|
|
290
|
+
class="w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm text-slate-900 transition-all duration-150 placeholder:text-slate-500/60 focus:border-blue-600 focus:outline-none focus:ring-3 focus:ring-blue-600/10"
|
|
291
|
+
type="text"
|
|
292
|
+
placeholder="Brief description of your issue"
|
|
293
|
+
bind:value={form.title}
|
|
294
|
+
required
|
|
295
|
+
/>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
<!-- Dynamic questions -->
|
|
299
|
+
{#each orderedQuestions as question (question.id)}
|
|
300
|
+
<div class="space-y-1.5">
|
|
301
|
+
<label class="text-sm font-semibold text-slate-900">
|
|
302
|
+
{question.label}{#if question.required}<span class="text-red-600"> *</span>{/if}
|
|
303
|
+
</label>
|
|
304
|
+
|
|
305
|
+
{#if question.type === "TEXT"}
|
|
306
|
+
<input
|
|
307
|
+
class="w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm text-slate-900 transition-all duration-150 focus:border-blue-600 focus:outline-none focus:ring-3 focus:ring-blue-600/10"
|
|
308
|
+
type="text"
|
|
309
|
+
value={String(form.answers[question.id] || "")}
|
|
310
|
+
oninput={(e) => updateAnswer(question.id, e.currentTarget.value)}
|
|
311
|
+
/>
|
|
312
|
+
{:else if question.type === "TEXTAREA"}
|
|
313
|
+
<textarea
|
|
314
|
+
class="min-h-[100px] w-full resize-y rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm text-slate-900 transition-all duration-150 focus:border-blue-600 focus:outline-none focus:ring-3 focus:ring-blue-600/10"
|
|
315
|
+
value={String(form.answers[question.id] || "")}
|
|
316
|
+
oninput={(e) => updateAnswer(question.id, e.currentTarget.value)}
|
|
317
|
+
></textarea>
|
|
318
|
+
{:else if question.type === "DATE"}
|
|
319
|
+
<input
|
|
320
|
+
class="w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm text-slate-900 transition-all duration-150 focus:border-blue-600 focus:outline-none focus:ring-3 focus:ring-blue-600/10"
|
|
321
|
+
type="date"
|
|
322
|
+
value={String(form.answers[question.id] || "")}
|
|
323
|
+
oninput={(e) => updateAnswer(question.id, e.currentTarget.value)}
|
|
324
|
+
/>
|
|
325
|
+
{:else if question.type === "SELECT"}
|
|
326
|
+
<select
|
|
327
|
+
class="w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm text-slate-900 transition-all duration-150 focus:border-blue-600 focus:outline-none focus:ring-3 focus:ring-blue-600/10"
|
|
328
|
+
value={String(form.answers[question.id] || "")}
|
|
329
|
+
onchange={(e) => updateAnswer(question.id, e.currentTarget.value)}
|
|
330
|
+
>
|
|
331
|
+
<option value="">Select an option</option>
|
|
332
|
+
{#each question.options || [] as option (option.id)}
|
|
333
|
+
<option value={option.value}>{option.label}</option>
|
|
334
|
+
{/each}
|
|
335
|
+
</select>
|
|
336
|
+
{:else if question.type === "CHECKBOX"}
|
|
337
|
+
<div class="space-y-2 pt-1">
|
|
338
|
+
{#each question.options || [] as option (option.id)}
|
|
339
|
+
<label class="flex items-center gap-2.5 text-sm text-slate-900">
|
|
340
|
+
<input
|
|
341
|
+
type="checkbox"
|
|
342
|
+
class="h-4 w-4 rounded accent-blue-600"
|
|
343
|
+
checked={Array.isArray(form.answers[question.id]) && (form.answers[question.id] as string[]).includes(option.value)}
|
|
344
|
+
onchange={(e) => toggleCheckboxValue(question.id, option.value, e.currentTarget.checked)}
|
|
345
|
+
/>
|
|
346
|
+
<span>{option.label}</span>
|
|
347
|
+
</label>
|
|
348
|
+
{/each}
|
|
349
|
+
</div>
|
|
350
|
+
{:else if question.type === "FILE"}
|
|
351
|
+
{#if form.answers[question.id] instanceof File}
|
|
352
|
+
<div class="flex flex-wrap gap-1.5">
|
|
353
|
+
<span class="inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-slate-50 px-3 py-1.5 text-xs text-slate-500">
|
|
354
|
+
{(form.answers[question.id] as File).name}
|
|
355
|
+
<button
|
|
356
|
+
type="button"
|
|
357
|
+
onclick={() => updateAnswer(question.id, null)}
|
|
358
|
+
class="flex cursor-pointer items-center border-none bg-transparent p-0 text-slate-500 transition-colors hover:text-red-600"
|
|
359
|
+
aria-label="Remove file"
|
|
360
|
+
>
|
|
361
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
|
|
362
|
+
</button>
|
|
363
|
+
</span>
|
|
364
|
+
</div>
|
|
365
|
+
{:else}
|
|
366
|
+
<input
|
|
367
|
+
class="w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm text-slate-900 transition-all duration-150 focus:border-blue-600 focus:outline-none focus:ring-3 focus:ring-blue-600/10"
|
|
368
|
+
type="file"
|
|
369
|
+
accept="image/jpeg,image/png,image/gif,image/webp"
|
|
370
|
+
onchange={(e) => {
|
|
371
|
+
const input = e.currentTarget as HTMLInputElement;
|
|
372
|
+
const file = input.files?.[0] || null;
|
|
373
|
+
if (file && !["image/png","image/jpeg","image/gif","image/webp"].includes(file.type)) {
|
|
374
|
+
input.value = "";
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
updateAnswer(question.id, file);
|
|
378
|
+
}}
|
|
379
|
+
/>
|
|
380
|
+
{/if}
|
|
381
|
+
{/if}
|
|
382
|
+
</div>
|
|
383
|
+
{/each}
|
|
384
|
+
|
|
385
|
+
<!-- Actions -->
|
|
386
|
+
<div class="flex items-center gap-3 pt-2">
|
|
387
|
+
<button
|
|
388
|
+
class="inline-flex flex-1 items-center justify-center gap-2 rounded-lg border-none bg-blue-600 px-5 py-2.5 text-sm font-semibold text-white cursor-pointer transition-all hover:bg-blue-700 hover:-translate-y-px hover:shadow-lg active:translate-y-0 disabled:opacity-60 disabled:cursor-not-allowed"
|
|
389
|
+
type="submit"
|
|
390
|
+
disabled={isSubmitting}
|
|
391
|
+
>
|
|
392
|
+
{#if isSubmitting}
|
|
393
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="animate-spin"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
|
|
394
|
+
Submitting...
|
|
395
|
+
{:else}
|
|
396
|
+
Submit
|
|
397
|
+
{/if}
|
|
398
|
+
</button>
|
|
399
|
+
</div>
|
|
400
|
+
</form>
|
|
401
|
+
</div>
|
|
402
|
+
{/if}
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
{/if}
|