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