@vicket/create-support 1.1.1
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/README.md +52 -0
- package/bin/create-vicket-support.js +389 -0
- package/package.json +18 -0
- package/templates/next/src/app/api/vicket/[...path]/route.ts +59 -0
- package/templates/next/src/app/components/vicket/TicketDialog.tsx +514 -0
- package/templates/next/src/app/support/page.tsx +358 -0
- package/templates/next/src/app/ticket/page.tsx +483 -0
- package/templates/next/src/app/utils/vicket/api.ts +149 -0
- package/templates/next/src/app/utils/vicket/types.ts +85 -0
- package/templates/next/src/app/utils/vicket/utils.ts +49 -0
- package/templates/next/src/app/vicket.css +1325 -0
- package/templates/nuxt/app/assets/css/vicket.css +1325 -0
- package/templates/nuxt/app/components/VicketTicketDialog.vue +499 -0
- package/templates/nuxt/app/composables/useVicket.ts +274 -0
- package/templates/nuxt/app/pages/support.vue +303 -0
- package/templates/nuxt/app/pages/ticket.vue +434 -0
- package/templates/nuxt/server/api/vicket/[...path].ts +85 -0
- package/templates/sveltekit/src/lib/vicket/TicketDialog.svelte +459 -0
- package/templates/sveltekit/src/lib/vicket/api.ts +162 -0
- package/templates/sveltekit/src/lib/vicket/types.ts +87 -0
- package/templates/sveltekit/src/lib/vicket/utils.ts +55 -0
- package/templates/sveltekit/src/lib/vicket.css +1325 -0
- package/templates/sveltekit/src/routes/api/vicket/[...path]/+server.ts +77 -0
- package/templates/sveltekit/src/routes/support/+page.svelte +316 -0
- package/templates/sveltekit/src/routes/ticket/+page.svelte +418 -0
- package/templates-tailwind/next/src/app/api/vicket/init/route.ts +24 -0
- package/templates-tailwind/next/src/app/api/vicket/messages/route.ts +36 -0
- package/templates-tailwind/next/src/app/api/vicket/thread/route.ts +27 -0
- package/templates-tailwind/next/src/app/api/vicket/tickets/route.ts +37 -0
- package/templates-tailwind/next/src/app/support/page.tsx +5 -0
- package/templates-tailwind/next/src/app/ticket/page.tsx +10 -0
- package/templates-tailwind/next/src/components/vicket/support-page.tsx +359 -0
- package/templates-tailwind/next/src/components/vicket/ticket-dialog.tsx +306 -0
- package/templates-tailwind/next/src/components/vicket/ticket-page.tsx +425 -0
- package/templates-tailwind/next/src/lib/vicket.ts +257 -0
- package/templates-tailwind/nuxt/app/components/VicketSupportPage.vue +317 -0
- package/templates-tailwind/nuxt/app/components/VicketTicketDialog.vue +444 -0
- package/templates-tailwind/nuxt/app/components/VicketTicketPage.vue +449 -0
- package/templates-tailwind/nuxt/app/composables/use-vicket.ts +249 -0
- package/templates-tailwind/nuxt/app/pages/support.vue +3 -0
- package/templates-tailwind/nuxt/app/pages/ticket.vue +3 -0
- package/templates-tailwind/nuxt/server/api/vicket/init.get.ts +22 -0
- package/templates-tailwind/nuxt/server/api/vicket/messages.post.ts +56 -0
- package/templates-tailwind/nuxt/server/api/vicket/thread.get.ts +26 -0
- package/templates-tailwind/nuxt/server/api/vicket/tickets.post.ts +53 -0
- package/templates-tailwind/sveltekit/src/lib/vicket/SupportPage.svelte +395 -0
- package/templates-tailwind/sveltekit/src/lib/vicket/TicketDialog.svelte +406 -0
- package/templates-tailwind/sveltekit/src/lib/vicket/TicketPage.svelte +465 -0
- package/templates-tailwind/sveltekit/src/lib/vicket/index.ts +257 -0
- package/templates-tailwind/sveltekit/src/routes/api/vicket/init/+server.ts +22 -0
- package/templates-tailwind/sveltekit/src/routes/api/vicket/messages/+server.ts +40 -0
- package/templates-tailwind/sveltekit/src/routes/api/vicket/thread/+server.ts +25 -0
- package/templates-tailwind/sveltekit/src/routes/api/vicket/tickets/+server.ts +37 -0
- package/templates-tailwind/sveltekit/src/routes/support/+page.svelte +5 -0
- package/templates-tailwind/sveltekit/src/routes/ticket/+page.svelte +5 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from "svelte";
|
|
3
|
+
import { page } from "$app/state";
|
|
4
|
+
import type { TicketThread } from "$lib/vicket/types";
|
|
5
|
+
import { cn, sanitizeHtml, stripHtml, formatDate, isFileAnswer, formatAnswerText } from "$lib/vicket/utils";
|
|
6
|
+
import { AUTHOR_LABELS, fetchTicketThread, sendReply } from "$lib/vicket/api";
|
|
7
|
+
|
|
8
|
+
/* ---------------------------------------------- */
|
|
9
|
+
/* State */
|
|
10
|
+
/* ---------------------------------------------- */
|
|
11
|
+
let thread = $state<TicketThread | null>(null);
|
|
12
|
+
let content = $state("");
|
|
13
|
+
let files = $state<File[]>([]);
|
|
14
|
+
let isLoading = $state(true);
|
|
15
|
+
let isSending = $state(false);
|
|
16
|
+
let error = $state("");
|
|
17
|
+
let success = $state("");
|
|
18
|
+
|
|
19
|
+
/* ---------------------------------------------- */
|
|
20
|
+
/* Derived */
|
|
21
|
+
/* ---------------------------------------------- */
|
|
22
|
+
let token = $derived(page.url.searchParams.get("token") || "");
|
|
23
|
+
let hasToken = $derived(token.trim().length > 0);
|
|
24
|
+
|
|
25
|
+
let firstReporterMessage = $derived.by(() => {
|
|
26
|
+
if (!thread?.messages || thread.messages.length === 0) return null;
|
|
27
|
+
const sorted = [...thread.messages].sort(
|
|
28
|
+
(a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime(),
|
|
29
|
+
);
|
|
30
|
+
// Only treat as description if the very first message is from the reporter
|
|
31
|
+
return sorted[0].author_type === "reporter" ? sorted[0] : null;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
let sortedMessages = $derived.by(() => {
|
|
35
|
+
if (!thread?.messages) return [];
|
|
36
|
+
return [...thread.messages]
|
|
37
|
+
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())
|
|
38
|
+
.filter((m) => !firstReporterMessage || m.id !== firstReporterMessage.id);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
let summaryAnswers = $derived.by(() => {
|
|
42
|
+
if (!thread?.answers) return [];
|
|
43
|
+
return thread.answers.filter((answer) => {
|
|
44
|
+
if (answer.attachments && answer.attachments.length > 0) return true;
|
|
45
|
+
if (answer.answer && answer.answer.trim().length > 0) return true;
|
|
46
|
+
return false;
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/* ---------------------------------------------- */
|
|
51
|
+
/* Functions */
|
|
52
|
+
/* ---------------------------------------------- */
|
|
53
|
+
function removeFile(index: number) {
|
|
54
|
+
files = files.filter((_, i) => i !== index);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function onFileChange(event: Event) {
|
|
58
|
+
const input = event.target as HTMLInputElement;
|
|
59
|
+
const newFiles = Array.from(input.files || []);
|
|
60
|
+
files = [...files, ...newFiles];
|
|
61
|
+
input.value = "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function loadThread() {
|
|
65
|
+
if (!hasToken) {
|
|
66
|
+
isLoading = false;
|
|
67
|
+
error = "Missing ticket token in URL.";
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
isLoading = true;
|
|
72
|
+
error = "";
|
|
73
|
+
try {
|
|
74
|
+
const data = await fetchTicketThread(token);
|
|
75
|
+
thread = data;
|
|
76
|
+
} catch (loadError) {
|
|
77
|
+
error = loadError instanceof Error ? loadError.message : "Unexpected error.";
|
|
78
|
+
} finally {
|
|
79
|
+
isLoading = false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function onSubmitReply(event: SubmitEvent) {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
error = "";
|
|
86
|
+
success = "";
|
|
87
|
+
|
|
88
|
+
if (!content.trim() && files.length === 0) {
|
|
89
|
+
error = "Reply content is required.";
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!hasToken) {
|
|
94
|
+
error = "Missing ticket token.";
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
isSending = true;
|
|
99
|
+
try {
|
|
100
|
+
await sendReply(token, content.trim(), files);
|
|
101
|
+
content = "";
|
|
102
|
+
files = [];
|
|
103
|
+
success = "Reply sent.";
|
|
104
|
+
await loadThread();
|
|
105
|
+
} catch (replyError) {
|
|
106
|
+
error = replyError instanceof Error ? replyError.message : "Unexpected error.";
|
|
107
|
+
} finally {
|
|
108
|
+
isSending = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onMount(() => {
|
|
113
|
+
void loadThread();
|
|
114
|
+
});
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<div class="vk-shell">
|
|
118
|
+
<div class="vk-page vk-animate-in">
|
|
119
|
+
<!-- Back link -->
|
|
120
|
+
<div class="vk-ticket-badges">
|
|
121
|
+
<a href="/support" class="vk-back-link">
|
|
122
|
+
← Back to support
|
|
123
|
+
</a>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<!-- Alerts -->
|
|
127
|
+
{#if error}
|
|
128
|
+
<div class={cn("vk-alert vk-slide-up", "error")} role="alert">
|
|
129
|
+
<span>⚠</span>
|
|
130
|
+
<span>{error}</span>
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
onclick={() => (error = "")}
|
|
134
|
+
class="vk-alert-dismiss"
|
|
135
|
+
aria-label="Dismiss"
|
|
136
|
+
>
|
|
137
|
+
✕
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
{/if}
|
|
141
|
+
{#if success}
|
|
142
|
+
<div class={cn("vk-alert vk-slide-up", "success")} role="alert">
|
|
143
|
+
<span>✓</span>
|
|
144
|
+
<span>{success}</span>
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
onclick={() => (success = "")}
|
|
148
|
+
class="vk-alert-dismiss"
|
|
149
|
+
aria-label="Dismiss"
|
|
150
|
+
>
|
|
151
|
+
✕
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
{/if}
|
|
155
|
+
|
|
156
|
+
<!-- Loading skeleton -->
|
|
157
|
+
{#if isLoading}
|
|
158
|
+
<div class="vk-stack vk-animate-in">
|
|
159
|
+
<!-- Header skeleton -->
|
|
160
|
+
<div>
|
|
161
|
+
<div class="vk-skeleton" aria-hidden="true">
|
|
162
|
+
|
|
163
|
+
</div>
|
|
164
|
+
<div class="vk-compose-files">
|
|
165
|
+
<span class="vk-skeleton" aria-hidden="true"> </span>
|
|
166
|
+
<span class="vk-skeleton" aria-hidden="true"> </span>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
<!-- Messages skeleton -->
|
|
170
|
+
<div class="vk-section-card">
|
|
171
|
+
<div class="vk-section-header">
|
|
172
|
+
<span class="vk-skeleton" aria-hidden="true"> </span>
|
|
173
|
+
</div>
|
|
174
|
+
{#each [1, 2, 3] as i (i)}
|
|
175
|
+
<div class="vk-message">
|
|
176
|
+
<div class="vk-avatar vk-skeleton" aria-hidden="true"> </div>
|
|
177
|
+
<div class="vk-message-content">
|
|
178
|
+
<div class="vk-skeleton" aria-hidden="true"> </div>
|
|
179
|
+
<div class="vk-skeleton" aria-hidden="true"> </div>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
{/each}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
{/if}
|
|
186
|
+
|
|
187
|
+
<!-- Thread -->
|
|
188
|
+
{#if !isLoading && thread}
|
|
189
|
+
<div class="vk-stack">
|
|
190
|
+
<!-- Header -->
|
|
191
|
+
<div>
|
|
192
|
+
<div class="vk-ticket-header">
|
|
193
|
+
<h1 class="vk-ticket-title">{thread.title}</h1>
|
|
194
|
+
{#if thread.id}
|
|
195
|
+
<span class={cn("vk-badge", "id")}>
|
|
196
|
+
#{thread.id.slice(0, 8)}
|
|
197
|
+
</span>
|
|
198
|
+
{/if}
|
|
199
|
+
</div>
|
|
200
|
+
{#if (thread.status?.label && thread.status.label.toLowerCase() !== "open") || (thread.priority?.label && thread.priority.label.toLowerCase() !== "low")}
|
|
201
|
+
<div class="vk-ticket-badges">
|
|
202
|
+
{#if thread.status?.label && thread.status.label.toLowerCase() !== "open"}
|
|
203
|
+
<span class={cn("vk-badge", "status")}>
|
|
204
|
+
{thread.status.label}
|
|
205
|
+
</span>
|
|
206
|
+
{/if}
|
|
207
|
+
{#if thread.priority?.label && thread.priority.label.toLowerCase() !== "low"}
|
|
208
|
+
<span class={cn("vk-badge", "priority")}>
|
|
209
|
+
{thread.priority.label}
|
|
210
|
+
</span>
|
|
211
|
+
{/if}
|
|
212
|
+
</div>
|
|
213
|
+
{/if}
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<!-- Summary (ticket form answers) -->
|
|
217
|
+
{#if summaryAnswers.length > 0}
|
|
218
|
+
<div class="vk-section-card">
|
|
219
|
+
<div class="vk-section-header">
|
|
220
|
+
<span class="vk-section-header-icon">📝</span>
|
|
221
|
+
<h2>Summary</h2>
|
|
222
|
+
</div>
|
|
223
|
+
<div class="vk-section-body">
|
|
224
|
+
<div class="vk-summary-list">
|
|
225
|
+
{#each summaryAnswers as answer (answer.id)}
|
|
226
|
+
<div class="vk-summary-item">
|
|
227
|
+
<p class="vk-summary-label">{answer.question_label || "Question"}</p>
|
|
228
|
+
{#if answer.attachments && answer.attachments.length > 0}
|
|
229
|
+
<div class="vk-attachments">
|
|
230
|
+
{#each answer.attachments as attachment (attachment.id)}
|
|
231
|
+
<a
|
|
232
|
+
class="vk-attachment"
|
|
233
|
+
href={attachment.url}
|
|
234
|
+
rel="noopener noreferrer"
|
|
235
|
+
target="_blank"
|
|
236
|
+
>
|
|
237
|
+
📎 <span>{attachment.original_filename}</span>
|
|
238
|
+
</a>
|
|
239
|
+
{/each}
|
|
240
|
+
</div>
|
|
241
|
+
{:else if isFileAnswer(answer.answer)}
|
|
242
|
+
<p class="vk-summary-value muted">File uploaded</p>
|
|
243
|
+
{:else}
|
|
244
|
+
<p class="vk-summary-value">{formatAnswerText(answer.answer) || "-"}</p>
|
|
245
|
+
{/if}
|
|
246
|
+
</div>
|
|
247
|
+
{/each}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
{/if}
|
|
252
|
+
|
|
253
|
+
<!-- Description (first reporter message) -->
|
|
254
|
+
{#if firstReporterMessage}
|
|
255
|
+
<div class="vk-section-card">
|
|
256
|
+
<div class="vk-section-header">
|
|
257
|
+
<span class="vk-section-header-icon">📄</span>
|
|
258
|
+
<h2>Description</h2>
|
|
259
|
+
</div>
|
|
260
|
+
<div class="vk-section-body">
|
|
261
|
+
<div class="vk-description-content vk-message-html">
|
|
262
|
+
{@html sanitizeHtml(firstReporterMessage.content)}
|
|
263
|
+
</div>
|
|
264
|
+
{#if firstReporterMessage.attachments && firstReporterMessage.attachments.length > 0}
|
|
265
|
+
<div class="vk-attachments">
|
|
266
|
+
{#each firstReporterMessage.attachments as att (att.id)}
|
|
267
|
+
<a
|
|
268
|
+
href={att.url}
|
|
269
|
+
target="_blank"
|
|
270
|
+
rel="noopener noreferrer"
|
|
271
|
+
class="vk-attachment"
|
|
272
|
+
>
|
|
273
|
+
📎 <span>{att.original_filename}</span>
|
|
274
|
+
</a>
|
|
275
|
+
{/each}
|
|
276
|
+
</div>
|
|
277
|
+
{/if}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
{/if}
|
|
281
|
+
|
|
282
|
+
<!-- Comments card -->
|
|
283
|
+
<div class="vk-section-card">
|
|
284
|
+
<div class="vk-section-header">
|
|
285
|
+
<span class="vk-section-header-icon">💬</span>
|
|
286
|
+
<h2>Comments</h2>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<!-- Compose area -->
|
|
290
|
+
<div class="vk-compose">
|
|
291
|
+
<form class="vk-stack" onsubmit={onSubmitReply}>
|
|
292
|
+
<textarea
|
|
293
|
+
class="vk-textarea"
|
|
294
|
+
bind:value={content}
|
|
295
|
+
placeholder="Write your reply..."
|
|
296
|
+
></textarea>
|
|
297
|
+
|
|
298
|
+
<div class="vk-compose-row">
|
|
299
|
+
<!-- File input -->
|
|
300
|
+
<div class="vk-compose-files">
|
|
301
|
+
<label class="vk-browse-btn">
|
|
302
|
+
📎 Browse files
|
|
303
|
+
<input
|
|
304
|
+
type="file"
|
|
305
|
+
multiple
|
|
306
|
+
onchange={onFileChange}
|
|
307
|
+
/>
|
|
308
|
+
</label>
|
|
309
|
+
{#each files as file, i}
|
|
310
|
+
<span class="vk-file-chip">
|
|
311
|
+
📎
|
|
312
|
+
<span class="vk-file-chip-name">{file.name}</span>
|
|
313
|
+
<button
|
|
314
|
+
type="button"
|
|
315
|
+
onclick={() => removeFile(i)}
|
|
316
|
+
aria-label="Remove {file.name}"
|
|
317
|
+
>
|
|
318
|
+
✕
|
|
319
|
+
</button>
|
|
320
|
+
</span>
|
|
321
|
+
{/each}
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<!-- Send -->
|
|
325
|
+
<button
|
|
326
|
+
class="vk-button primary"
|
|
327
|
+
disabled={isSending}
|
|
328
|
+
type="submit"
|
|
329
|
+
>
|
|
330
|
+
{#if isSending}
|
|
331
|
+
<span class="vk-spinner"></span>
|
|
332
|
+
Sending...
|
|
333
|
+
{:else}
|
|
334
|
+
Send
|
|
335
|
+
{/if}
|
|
336
|
+
</button>
|
|
337
|
+
</div>
|
|
338
|
+
</form>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<!-- Message list -->
|
|
342
|
+
{#if sortedMessages.length === 0}
|
|
343
|
+
<div class="vk-empty-state">
|
|
344
|
+
<div class="vk-empty-icon">💬</div>
|
|
345
|
+
<p class="vk-empty-text">
|
|
346
|
+
No messages yet. Be the first to reply!
|
|
347
|
+
</p>
|
|
348
|
+
</div>
|
|
349
|
+
{:else}
|
|
350
|
+
<div class="vk-message-list">
|
|
351
|
+
{#each sortedMessages as message, index (message.id)}
|
|
352
|
+
{#if index > 0}
|
|
353
|
+
<hr class="vk-message-divider" />
|
|
354
|
+
{/if}
|
|
355
|
+
|
|
356
|
+
{#if message.author_type === "system"}
|
|
357
|
+
<!-- System message -->
|
|
358
|
+
<div class="vk-message">
|
|
359
|
+
<div class={cn("vk-avatar", "vk-avatar-system")} aria-hidden="true">
|
|
360
|
+
S
|
|
361
|
+
</div>
|
|
362
|
+
<div class="vk-message-content">
|
|
363
|
+
<div class="vk-message-meta">
|
|
364
|
+
<span class="vk-author-name system">System</span>
|
|
365
|
+
<span class="vk-message-time">{formatDate(message.created_at)}</span>
|
|
366
|
+
</div>
|
|
367
|
+
<p class="vk-system-text">
|
|
368
|
+
{stripHtml(message.content)}
|
|
369
|
+
</p>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
{:else}
|
|
373
|
+
<!-- Regular message -->
|
|
374
|
+
<div class="vk-message">
|
|
375
|
+
<div class={cn("vk-avatar", `vk-avatar-${message.author_type}`)} aria-hidden="true">
|
|
376
|
+
{(AUTHOR_LABELS[message.author_type] || "?")[0]}
|
|
377
|
+
</div>
|
|
378
|
+
<div class="vk-message-content">
|
|
379
|
+
<div class="vk-message-meta">
|
|
380
|
+
<span class="vk-author-name">
|
|
381
|
+
{AUTHOR_LABELS[message.author_type] || message.author_type}
|
|
382
|
+
</span>
|
|
383
|
+
<span class="vk-message-time">{formatDate(message.created_at)}</span>
|
|
384
|
+
</div>
|
|
385
|
+
<div class={cn("vk-message-bubble", message.author_type === "user" && "support")}>
|
|
386
|
+
<div class="vk-message-html">
|
|
387
|
+
{@html sanitizeHtml(message.content)}
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
{#if message.attachments && message.attachments.length > 0}
|
|
391
|
+
<div class="vk-attachments">
|
|
392
|
+
{#each message.attachments as attachment (attachment.id)}
|
|
393
|
+
<a
|
|
394
|
+
class="vk-attachment"
|
|
395
|
+
href={attachment.url}
|
|
396
|
+
rel="noopener noreferrer"
|
|
397
|
+
target="_blank"
|
|
398
|
+
>
|
|
399
|
+
📎 <span>{attachment.original_filename}</span>
|
|
400
|
+
</a>
|
|
401
|
+
{/each}
|
|
402
|
+
</div>
|
|
403
|
+
{/if}
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
{/if}
|
|
407
|
+
{/each}
|
|
408
|
+
</div>
|
|
409
|
+
{/if}
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
{/if}
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<style>
|
|
417
|
+
@import '$lib/vicket.css';
|
|
418
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
const VICKET_API_URL = (process.env.VICKET_API_URL || "").replace(/\/+$/, "");
|
|
4
|
+
const VICKET_API_KEY = process.env.VICKET_API_KEY || "";
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
if (!VICKET_API_URL || !VICKET_API_KEY) {
|
|
8
|
+
return NextResponse.json(
|
|
9
|
+
{ error: "Missing VICKET_API_URL or VICKET_API_KEY server environment variables." },
|
|
10
|
+
{ status: 500 },
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const upstream = await fetch(`${VICKET_API_URL}/public/support/init`, {
|
|
15
|
+
method: "GET",
|
|
16
|
+
headers: { "X-Api-Key": VICKET_API_KEY, "Content-Type": "application/json" },
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const body = await upstream.arrayBuffer();
|
|
20
|
+
return new NextResponse(body, {
|
|
21
|
+
status: upstream.status,
|
|
22
|
+
headers: { "Content-Type": upstream.headers.get("Content-Type") || "application/json" },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
const VICKET_API_URL = (process.env.VICKET_API_URL || "").replace(/\/+$/, "");
|
|
4
|
+
const VICKET_API_KEY = process.env.VICKET_API_KEY || "";
|
|
5
|
+
|
|
6
|
+
export async function POST(req: NextRequest) {
|
|
7
|
+
if (!VICKET_API_URL || !VICKET_API_KEY) {
|
|
8
|
+
return NextResponse.json(
|
|
9
|
+
{ error: "Missing VICKET_API_URL or VICKET_API_KEY server environment variables." },
|
|
10
|
+
{ status: 500 },
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const token = req.nextUrl.searchParams.get("token") || "";
|
|
15
|
+
const url = `${VICKET_API_URL}/public/support/ticket/messages?token=${encodeURIComponent(token)}`;
|
|
16
|
+
|
|
17
|
+
const contentType = req.headers.get("content-type") || "";
|
|
18
|
+
const headers: HeadersInit = { "X-Api-Key": VICKET_API_KEY };
|
|
19
|
+
let body: BodyInit;
|
|
20
|
+
|
|
21
|
+
if (contentType.includes("multipart/form-data")) {
|
|
22
|
+
body = await req.blob();
|
|
23
|
+
headers["Content-Type"] = contentType;
|
|
24
|
+
} else {
|
|
25
|
+
body = await req.text();
|
|
26
|
+
headers["Content-Type"] = "application/json";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const upstream = await fetch(url, { method: "POST", headers, body });
|
|
30
|
+
|
|
31
|
+
const responseBody = await upstream.arrayBuffer();
|
|
32
|
+
return new NextResponse(responseBody, {
|
|
33
|
+
status: upstream.status,
|
|
34
|
+
headers: { "Content-Type": upstream.headers.get("Content-Type") || "application/json" },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
const VICKET_API_URL = (process.env.VICKET_API_URL || "").replace(/\/+$/, "");
|
|
4
|
+
const VICKET_API_KEY = process.env.VICKET_API_KEY || "";
|
|
5
|
+
|
|
6
|
+
export async function GET(req: NextRequest) {
|
|
7
|
+
if (!VICKET_API_URL || !VICKET_API_KEY) {
|
|
8
|
+
return NextResponse.json(
|
|
9
|
+
{ error: "Missing VICKET_API_URL or VICKET_API_KEY server environment variables." },
|
|
10
|
+
{ status: 500 },
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const token = req.nextUrl.searchParams.get("token") || "";
|
|
15
|
+
const url = `${VICKET_API_URL}/public/support/ticket?token=${encodeURIComponent(token)}`;
|
|
16
|
+
|
|
17
|
+
const upstream = await fetch(url, {
|
|
18
|
+
method: "GET",
|
|
19
|
+
headers: { "X-Api-Key": VICKET_API_KEY, "Content-Type": "application/json" },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const body = await upstream.arrayBuffer();
|
|
23
|
+
return new NextResponse(body, {
|
|
24
|
+
status: upstream.status,
|
|
25
|
+
headers: { "Content-Type": upstream.headers.get("Content-Type") || "application/json" },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
const VICKET_API_URL = (process.env.VICKET_API_URL || "").replace(/\/+$/, "");
|
|
4
|
+
const VICKET_API_KEY = process.env.VICKET_API_KEY || "";
|
|
5
|
+
|
|
6
|
+
export async function POST(req: NextRequest) {
|
|
7
|
+
if (!VICKET_API_URL || !VICKET_API_KEY) {
|
|
8
|
+
return NextResponse.json(
|
|
9
|
+
{ error: "Missing VICKET_API_URL or VICKET_API_KEY server environment variables." },
|
|
10
|
+
{ status: 500 },
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const contentType = req.headers.get("content-type") || "";
|
|
15
|
+
const headers: HeadersInit = { "X-Api-Key": VICKET_API_KEY };
|
|
16
|
+
let body: BodyInit;
|
|
17
|
+
|
|
18
|
+
if (contentType.includes("multipart/form-data")) {
|
|
19
|
+
body = await req.blob();
|
|
20
|
+
headers["Content-Type"] = contentType;
|
|
21
|
+
} else {
|
|
22
|
+
body = await req.text();
|
|
23
|
+
headers["Content-Type"] = "application/json";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const upstream = await fetch(`${VICKET_API_URL}/public/support/tickets`, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers,
|
|
29
|
+
body,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const responseBody = await upstream.arrayBuffer();
|
|
33
|
+
return new NextResponse(responseBody, {
|
|
34
|
+
status: upstream.status,
|
|
35
|
+
headers: { "Content-Type": upstream.headers.get("Content-Type") || "application/json" },
|
|
36
|
+
});
|
|
37
|
+
}
|