@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,274 +0,0 @@
|
|
|
1
|
-
/* ---------------------------------------------- */
|
|
2
|
-
/* Types */
|
|
3
|
-
/* ---------------------------------------------- */
|
|
4
|
-
export type TemplateOption = {
|
|
5
|
-
id: string;
|
|
6
|
-
label: string;
|
|
7
|
-
value: string;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export type TemplateQuestion = {
|
|
11
|
-
id: string;
|
|
12
|
-
label: string;
|
|
13
|
-
type: "TEXT" | "TEXTAREA" | "SELECT" | "CHECKBOX" | "DATE" | "FILE";
|
|
14
|
-
required: boolean;
|
|
15
|
-
order: number;
|
|
16
|
-
options?: TemplateOption[];
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export type Template = {
|
|
20
|
-
id: string;
|
|
21
|
-
name: string;
|
|
22
|
-
description: string;
|
|
23
|
-
questions: TemplateQuestion[];
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export type Article = {
|
|
27
|
-
id: string;
|
|
28
|
-
title: string;
|
|
29
|
-
slug: string;
|
|
30
|
-
content: string;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export type Faq = {
|
|
34
|
-
id: string;
|
|
35
|
-
question: string;
|
|
36
|
-
answer: string;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export type SupportInitResponse = {
|
|
40
|
-
success: boolean;
|
|
41
|
-
data?: {
|
|
42
|
-
website?: { name?: string };
|
|
43
|
-
templates: Template[];
|
|
44
|
-
articles?: Article[];
|
|
45
|
-
faqs?: Faq[];
|
|
46
|
-
};
|
|
47
|
-
error?: string;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export type FormValues = {
|
|
51
|
-
email: string;
|
|
52
|
-
title: string;
|
|
53
|
-
answers: Record<string, unknown>;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export type Attachment = {
|
|
57
|
-
id: string;
|
|
58
|
-
original_filename: string;
|
|
59
|
-
url: string;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export type Message = {
|
|
63
|
-
id: string;
|
|
64
|
-
content: string;
|
|
65
|
-
author_type: "reporter" | "user" | "system";
|
|
66
|
-
created_at: string;
|
|
67
|
-
attachments?: Attachment[];
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
export type TicketAnswer = {
|
|
71
|
-
id: string;
|
|
72
|
-
question_label: string;
|
|
73
|
-
answer: string;
|
|
74
|
-
attachments?: Attachment[];
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export type TicketThread = {
|
|
78
|
-
id: string;
|
|
79
|
-
title: string;
|
|
80
|
-
status?: { label: string };
|
|
81
|
-
priority?: { label: string };
|
|
82
|
-
messages: Message[];
|
|
83
|
-
answers?: TicketAnswer[];
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
/* ---------------------------------------------- */
|
|
87
|
-
/* Constants */
|
|
88
|
-
/* ---------------------------------------------- */
|
|
89
|
-
export const PROXY_BASE = "/api/vicket";
|
|
90
|
-
|
|
91
|
-
export const AUTHOR_LABELS: Record<string, string> = {
|
|
92
|
-
reporter: "You",
|
|
93
|
-
user: "Support",
|
|
94
|
-
system: "System",
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
export const initialFormValues: FormValues = { email: "", title: "", answers: {} };
|
|
98
|
-
|
|
99
|
-
/* ---------------------------------------------- */
|
|
100
|
-
/* Utilities */
|
|
101
|
-
/* ---------------------------------------------- */
|
|
102
|
-
export function cn(...classes: (string | false | null | undefined)[]): string {
|
|
103
|
-
return classes.filter(Boolean).join(" ");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function stripHtml(html: string): string {
|
|
107
|
-
return html.replace(/<[^>]*>/g, "");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function sanitizeHtml(html: string): string {
|
|
111
|
-
return html
|
|
112
|
-
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
113
|
-
.replace(/\son\w+="[^"]*"/gi, "")
|
|
114
|
-
.replace(/\son\w+='[^']*'/gi, "")
|
|
115
|
-
.replace(/href\s*=\s*"javascript:[^"]*"/gi, 'href="#"')
|
|
116
|
-
.replace(/href\s*=\s*'javascript:[^']*'/gi, "href='#'");
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function formatDate(iso: string): string {
|
|
120
|
-
try {
|
|
121
|
-
return new Intl.DateTimeFormat("en", {
|
|
122
|
-
month: "short",
|
|
123
|
-
day: "numeric",
|
|
124
|
-
hour: "numeric",
|
|
125
|
-
minute: "2-digit",
|
|
126
|
-
}).format(new Date(iso));
|
|
127
|
-
} catch {
|
|
128
|
-
return iso;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function isFileAnswer(answer: string): boolean {
|
|
133
|
-
return answer?.includes("__isFile:true") || answer?.includes("map[__isFile");
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function formatAnswerText(value: string): string {
|
|
137
|
-
if (!value) return "";
|
|
138
|
-
|
|
139
|
-
const trimmed = value.trim();
|
|
140
|
-
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
141
|
-
const rawItems = trimmed.slice(1, -1).trim();
|
|
142
|
-
return rawItems.length > 0 ? rawItems.split(/\s+/).join(", ") : "";
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return value;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/* ---------------------------------------------- */
|
|
149
|
-
/* API functions */
|
|
150
|
-
/* ---------------------------------------------- */
|
|
151
|
-
export async function fetchSupportInit(): Promise<NonNullable<SupportInitResponse["data"]>> {
|
|
152
|
-
const payload = await $fetch<SupportInitResponse>(`${PROXY_BASE}/init`, {
|
|
153
|
-
method: "GET",
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
if (!payload?.success || !payload?.data) {
|
|
157
|
-
throw new Error(payload?.error || "Failed to load support data.");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return payload.data;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export async function createTicket(body: {
|
|
164
|
-
email: string;
|
|
165
|
-
title: string;
|
|
166
|
-
templateId: string;
|
|
167
|
-
answers: Record<string, unknown>;
|
|
168
|
-
hasFiles: boolean;
|
|
169
|
-
fileQuestionIds: string[];
|
|
170
|
-
}): Promise<{ emailLimitReached?: boolean; warning?: string }> {
|
|
171
|
-
const payload = {
|
|
172
|
-
email: body.email,
|
|
173
|
-
title: body.title,
|
|
174
|
-
templateId: body.templateId,
|
|
175
|
-
answers: { ...body.answers },
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
let response: Response;
|
|
179
|
-
if (body.hasFiles) {
|
|
180
|
-
const formData = new FormData();
|
|
181
|
-
const normalizedAnswers: Record<string, unknown> = {};
|
|
182
|
-
for (const [questionId, answer] of Object.entries(payload.answers)) {
|
|
183
|
-
if (Array.isArray(answer) && answer.length > 0 && answer[0] instanceof File) {
|
|
184
|
-
(answer as File[]).forEach((f) => formData.append(`files[${questionId}]`, f));
|
|
185
|
-
normalizedAnswers[questionId] = "__isFile:true";
|
|
186
|
-
} else if (answer instanceof File) {
|
|
187
|
-
formData.append(`files[${questionId}]`, answer);
|
|
188
|
-
normalizedAnswers[questionId] = "__isFile:true";
|
|
189
|
-
} else {
|
|
190
|
-
normalizedAnswers[questionId] = answer;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
formData.append("data", JSON.stringify({ ...payload, answers: normalizedAnswers }));
|
|
194
|
-
response = await fetch(`${PROXY_BASE}/tickets`, {
|
|
195
|
-
method: "POST",
|
|
196
|
-
body: formData,
|
|
197
|
-
});
|
|
198
|
-
} else {
|
|
199
|
-
response = await fetch(`${PROXY_BASE}/tickets`, {
|
|
200
|
-
method: "POST",
|
|
201
|
-
headers: { "Content-Type": "application/json" },
|
|
202
|
-
body: JSON.stringify(payload),
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const responsePayload = (await response.json()) as {
|
|
207
|
-
error?: string;
|
|
208
|
-
success?: boolean;
|
|
209
|
-
data?: { email_limit_reached?: boolean; warning?: string };
|
|
210
|
-
};
|
|
211
|
-
if (!response.ok || !responsePayload?.success) {
|
|
212
|
-
throw new Error(responsePayload?.error || "Failed to create ticket.");
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return {
|
|
216
|
-
emailLimitReached: responsePayload.data?.email_limit_reached ?? false,
|
|
217
|
-
warning: responsePayload.data?.warning,
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export async function fetchTicketThread(token: string): Promise<TicketThread> {
|
|
222
|
-
const payload = await $fetch<{
|
|
223
|
-
success?: boolean;
|
|
224
|
-
error?: string;
|
|
225
|
-
error_code?: string;
|
|
226
|
-
data?: TicketThread;
|
|
227
|
-
}>(`${PROXY_BASE}/ticket?token=${encodeURIComponent(token)}`, {
|
|
228
|
-
method: "GET",
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
if (!payload?.success || !payload?.data) {
|
|
232
|
-
if (payload?.error_code === "ticket-link-expired") {
|
|
233
|
-
throw new Error("This link has expired. A new secure link has been sent to your email.");
|
|
234
|
-
}
|
|
235
|
-
throw new Error(payload?.error || "Failed to load ticket.");
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return payload.data;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export async function sendReply(token: string, content: string, files: File[]): Promise<void> {
|
|
242
|
-
const url = `${PROXY_BASE}/ticket/messages?token=${encodeURIComponent(token)}`;
|
|
243
|
-
|
|
244
|
-
let response: Response;
|
|
245
|
-
if (files.length > 0) {
|
|
246
|
-
const formData = new FormData();
|
|
247
|
-
formData.append("data", JSON.stringify({ content }));
|
|
248
|
-
for (const file of files) {
|
|
249
|
-
formData.append("files", file);
|
|
250
|
-
}
|
|
251
|
-
response = await fetch(url, {
|
|
252
|
-
method: "POST",
|
|
253
|
-
body: formData,
|
|
254
|
-
});
|
|
255
|
-
} else {
|
|
256
|
-
response = await fetch(url, {
|
|
257
|
-
method: "POST",
|
|
258
|
-
headers: { "Content-Type": "application/json" },
|
|
259
|
-
body: JSON.stringify({ content }),
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const responsePayload = (await response.json()) as {
|
|
264
|
-
success?: boolean;
|
|
265
|
-
error?: string;
|
|
266
|
-
error_code?: string;
|
|
267
|
-
};
|
|
268
|
-
if (!response.ok || !responsePayload?.success) {
|
|
269
|
-
if (responsePayload?.error_code === "ticket-link-expired") {
|
|
270
|
-
throw new Error("This link has expired. A new secure link has been sent to your email.");
|
|
271
|
-
}
|
|
272
|
-
throw new Error(responsePayload?.error || "Failed to send reply.");
|
|
273
|
-
}
|
|
274
|
-
}
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
/* ---------------------------------------------- */
|
|
2
|
-
/* API functions and constants */
|
|
3
|
-
/* ---------------------------------------------- */
|
|
4
|
-
|
|
5
|
-
import type {
|
|
6
|
-
FormValues,
|
|
7
|
-
SupportInitResponse,
|
|
8
|
-
TicketThread,
|
|
9
|
-
} from "./types";
|
|
10
|
-
|
|
11
|
-
// -- Constants -----------------------------------
|
|
12
|
-
|
|
13
|
-
/** Base path for the SvelteKit server-side proxy. */
|
|
14
|
-
export const PROXY_BASE = "/api/vicket";
|
|
15
|
-
|
|
16
|
-
/** Default empty form values. */
|
|
17
|
-
export const initialFormValues: FormValues = { email: "", title: "", answers: {} };
|
|
18
|
-
|
|
19
|
-
/** Human-readable labels for message author types. */
|
|
20
|
-
export const AUTHOR_LABELS: Record<string, string> = {
|
|
21
|
-
reporter: "You",
|
|
22
|
-
user: "Support",
|
|
23
|
-
system: "System",
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
// -- Support page API ----------------------------
|
|
27
|
-
|
|
28
|
-
/** Fetch initial support data (templates, articles, FAQs). */
|
|
29
|
-
export async function fetchSupportInit(): Promise<NonNullable<SupportInitResponse["data"]>> {
|
|
30
|
-
const response = await fetch(`${PROXY_BASE}/init`, {
|
|
31
|
-
method: "GET",
|
|
32
|
-
headers: { "Content-Type": "application/json" },
|
|
33
|
-
});
|
|
34
|
-
const payload = (await response.json()) as SupportInitResponse;
|
|
35
|
-
if (!response.ok || !payload?.success || !payload?.data) {
|
|
36
|
-
throw new Error(payload?.error || "Failed to load support data.");
|
|
37
|
-
}
|
|
38
|
-
return payload.data;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Create a new ticket, optionally with file uploads. */
|
|
42
|
-
export async function createTicket(body: {
|
|
43
|
-
email: string;
|
|
44
|
-
title: string;
|
|
45
|
-
templateId: string;
|
|
46
|
-
answers: Record<string, unknown>;
|
|
47
|
-
hasFiles: boolean;
|
|
48
|
-
fileQuestionIds: string[];
|
|
49
|
-
}): Promise<{ emailLimitReached?: boolean; warning?: string }> {
|
|
50
|
-
const payload = {
|
|
51
|
-
email: body.email,
|
|
52
|
-
title: body.title,
|
|
53
|
-
templateId: body.templateId,
|
|
54
|
-
answers: { ...body.answers },
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
let response: Response;
|
|
58
|
-
if (body.hasFiles) {
|
|
59
|
-
const formData = new FormData();
|
|
60
|
-
const normalizedAnswers: Record<string, unknown> = {};
|
|
61
|
-
for (const [questionId, answer] of Object.entries(payload.answers)) {
|
|
62
|
-
if (Array.isArray(answer) && answer.length > 0 && answer[0] instanceof File) {
|
|
63
|
-
(answer as File[]).forEach((f) => formData.append(`files[${questionId}]`, f));
|
|
64
|
-
normalizedAnswers[questionId] = "__isFile:true";
|
|
65
|
-
} else if (answer instanceof File) {
|
|
66
|
-
formData.append(`files[${questionId}]`, answer);
|
|
67
|
-
normalizedAnswers[questionId] = "__isFile:true";
|
|
68
|
-
} else {
|
|
69
|
-
normalizedAnswers[questionId] = answer;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
formData.append("data", JSON.stringify({ ...payload, answers: normalizedAnswers }));
|
|
73
|
-
response = await fetch(`${PROXY_BASE}/tickets`, {
|
|
74
|
-
method: "POST",
|
|
75
|
-
body: formData,
|
|
76
|
-
});
|
|
77
|
-
} else {
|
|
78
|
-
response = await fetch(`${PROXY_BASE}/tickets`, {
|
|
79
|
-
method: "POST",
|
|
80
|
-
headers: { "Content-Type": "application/json" },
|
|
81
|
-
body: JSON.stringify(payload),
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const responsePayload = (await response.json()) as {
|
|
86
|
-
error?: string;
|
|
87
|
-
success?: boolean;
|
|
88
|
-
data?: { email_limit_reached?: boolean; warning?: string };
|
|
89
|
-
};
|
|
90
|
-
if (!response.ok || !responsePayload?.success) {
|
|
91
|
-
throw new Error(responsePayload?.error || "Failed to create ticket.");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
emailLimitReached: responsePayload.data?.email_limit_reached ?? false,
|
|
96
|
-
warning: responsePayload.data?.warning,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// -- Ticket page API -----------------------------
|
|
101
|
-
|
|
102
|
-
/** Fetch a ticket thread by its secure token. */
|
|
103
|
-
export async function fetchTicketThread(token: string): Promise<TicketThread> {
|
|
104
|
-
const response = await fetch(
|
|
105
|
-
`${PROXY_BASE}/ticket?token=${encodeURIComponent(token)}`,
|
|
106
|
-
{
|
|
107
|
-
method: "GET",
|
|
108
|
-
headers: { "Content-Type": "application/json" },
|
|
109
|
-
},
|
|
110
|
-
);
|
|
111
|
-
const payload = (await response.json()) as {
|
|
112
|
-
success?: boolean;
|
|
113
|
-
error?: string;
|
|
114
|
-
error_code?: string;
|
|
115
|
-
data?: TicketThread;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
if (!response.ok || !payload?.success || !payload?.data) {
|
|
119
|
-
if (payload?.error_code === "ticket-link-expired") {
|
|
120
|
-
throw new Error("This link has expired. A new secure link has been sent to your email.");
|
|
121
|
-
}
|
|
122
|
-
throw new Error(payload?.error || "Failed to load ticket.");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return payload.data;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Send a reply message to a ticket thread. */
|
|
129
|
-
export async function sendReply(token: string, content: string, files: File[]): Promise<void> {
|
|
130
|
-
const url = `${PROXY_BASE}/ticket/messages?token=${encodeURIComponent(token)}`;
|
|
131
|
-
|
|
132
|
-
let response: Response;
|
|
133
|
-
if (files.length > 0) {
|
|
134
|
-
const formData = new FormData();
|
|
135
|
-
formData.append("data", JSON.stringify({ content }));
|
|
136
|
-
for (const file of files) {
|
|
137
|
-
formData.append("files", file);
|
|
138
|
-
}
|
|
139
|
-
response = await fetch(url, {
|
|
140
|
-
method: "POST",
|
|
141
|
-
body: formData,
|
|
142
|
-
});
|
|
143
|
-
} else {
|
|
144
|
-
response = await fetch(url, {
|
|
145
|
-
method: "POST",
|
|
146
|
-
headers: { "Content-Type": "application/json" },
|
|
147
|
-
body: JSON.stringify({ content }),
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const payload = (await response.json()) as {
|
|
152
|
-
success?: boolean;
|
|
153
|
-
error?: string;
|
|
154
|
-
error_code?: string;
|
|
155
|
-
};
|
|
156
|
-
if (!response.ok || !payload?.success) {
|
|
157
|
-
if (payload?.error_code === "ticket-link-expired") {
|
|
158
|
-
throw new Error("This link has expired. A new secure link has been sent to your email.");
|
|
159
|
-
}
|
|
160
|
-
throw new Error(payload?.error || "Failed to send reply.");
|
|
161
|
-
}
|
|
162
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/* ---------------------------------------------- */
|
|
2
|
-
/* Shared types for the Vicket support widget */
|
|
3
|
-
/* ---------------------------------------------- */
|
|
4
|
-
|
|
5
|
-
// -- Support page types --------------------------
|
|
6
|
-
export type TemplateOption = {
|
|
7
|
-
id: string;
|
|
8
|
-
label: string;
|
|
9
|
-
value: string;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type TemplateQuestion = {
|
|
13
|
-
id: string;
|
|
14
|
-
label: string;
|
|
15
|
-
type: "TEXT" | "TEXTAREA" | "SELECT" | "CHECKBOX" | "DATE" | "FILE";
|
|
16
|
-
required: boolean;
|
|
17
|
-
order: number;
|
|
18
|
-
options?: TemplateOption[];
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type Template = {
|
|
22
|
-
id: string;
|
|
23
|
-
name: string;
|
|
24
|
-
description: string;
|
|
25
|
-
questions: TemplateQuestion[];
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export type Article = {
|
|
29
|
-
id: string;
|
|
30
|
-
title: string;
|
|
31
|
-
slug: string;
|
|
32
|
-
content: string;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export type Faq = {
|
|
36
|
-
id: string;
|
|
37
|
-
question: string;
|
|
38
|
-
answer: string;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export type SupportInitResponse = {
|
|
42
|
-
success: boolean;
|
|
43
|
-
data?: {
|
|
44
|
-
website?: { name?: string };
|
|
45
|
-
templates: Template[];
|
|
46
|
-
articles?: Article[];
|
|
47
|
-
faqs?: Faq[];
|
|
48
|
-
};
|
|
49
|
-
error?: string;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export type FormValues = {
|
|
53
|
-
email: string;
|
|
54
|
-
title: string;
|
|
55
|
-
answers: Record<string, unknown>;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// -- Ticket page types ---------------------------
|
|
59
|
-
export type Attachment = {
|
|
60
|
-
id: string;
|
|
61
|
-
original_filename: string;
|
|
62
|
-
url: string;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export type Message = {
|
|
66
|
-
id: string;
|
|
67
|
-
content: string;
|
|
68
|
-
author_type: "reporter" | "user" | "system";
|
|
69
|
-
created_at: string;
|
|
70
|
-
attachments?: Attachment[];
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
export type TicketAnswer = {
|
|
74
|
-
id: string;
|
|
75
|
-
question_label: string;
|
|
76
|
-
answer: string;
|
|
77
|
-
attachments?: Attachment[];
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export type TicketThread = {
|
|
81
|
-
id: string;
|
|
82
|
-
title: string;
|
|
83
|
-
status?: { label: string };
|
|
84
|
-
priority?: { label: string };
|
|
85
|
-
messages: Message[];
|
|
86
|
-
answers?: TicketAnswer[];
|
|
87
|
-
};
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/* ---------------------------------------------- */
|
|
2
|
-
/* Shared utility functions */
|
|
3
|
-
/* ---------------------------------------------- */
|
|
4
|
-
|
|
5
|
-
/** Join class names, filtering out falsy values. */
|
|
6
|
-
export function cn(...classes: (string | boolean | undefined | null)[]): string {
|
|
7
|
-
return classes.filter(Boolean).join(" ");
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** Strip all HTML tags from a string. */
|
|
11
|
-
export function stripHtml(html: string): string {
|
|
12
|
-
return html.replace(/<[^>]*>/g, "");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Sanitize HTML by removing script tags, inline event handlers, and javascript: hrefs. */
|
|
16
|
-
export function sanitizeHtml(html: string): string {
|
|
17
|
-
return html
|
|
18
|
-
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
19
|
-
.replace(/\son\w+="[^"]*"/gi, "")
|
|
20
|
-
.replace(/\son\w+='[^']*'/gi, "")
|
|
21
|
-
.replace(/href\s*=\s*"javascript:[^"]*"/gi, 'href="#"')
|
|
22
|
-
.replace(/href\s*=\s*'javascript:[^']*'/gi, "href='#'");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Format an ISO date string for display. */
|
|
26
|
-
export function formatDate(iso: string): string {
|
|
27
|
-
try {
|
|
28
|
-
return new Intl.DateTimeFormat("en", {
|
|
29
|
-
month: "short",
|
|
30
|
-
day: "numeric",
|
|
31
|
-
hour: "numeric",
|
|
32
|
-
minute: "2-digit",
|
|
33
|
-
}).format(new Date(iso));
|
|
34
|
-
} catch {
|
|
35
|
-
return iso;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Check whether an answer value represents a file upload placeholder. */
|
|
40
|
-
export function isFileAnswer(answer: string): boolean {
|
|
41
|
-
return answer?.includes("__isFile:true") || answer?.includes("map[__isFile");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Format a raw answer value for display (e.g. unwrap bracket-delimited arrays). */
|
|
45
|
-
export function formatAnswerText(value: string): string {
|
|
46
|
-
if (!value) return "";
|
|
47
|
-
|
|
48
|
-
const trimmed = value.trim();
|
|
49
|
-
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
50
|
-
const rawItems = trimmed.slice(1, -1).trim();
|
|
51
|
-
return rawItems.length > 0 ? rawItems.split(/\s+/).join(", ") : "";
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return value;
|
|
55
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
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
|
-
}
|