@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,358 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import "../vicket.css";
|
|
5
|
+
import type { Article, Faq, Template } from "../utils/vicket/types";
|
|
6
|
+
import { cn, stripHtml, sanitizeHtml } from "../utils/vicket/utils";
|
|
7
|
+
import { fetchSupportInit } from "../utils/vicket/api";
|
|
8
|
+
import TicketDialog from "../components/vicket/TicketDialog";
|
|
9
|
+
|
|
10
|
+
/* ---------------------------------------------- */
|
|
11
|
+
/* Skeleton loader */
|
|
12
|
+
/* ---------------------------------------------- */
|
|
13
|
+
function HomeSkeleton() {
|
|
14
|
+
return (
|
|
15
|
+
<div className="vk-shell">
|
|
16
|
+
<div className="vk-page vk-animate-in">
|
|
17
|
+
<div className="vk-hero-row">
|
|
18
|
+
<div>
|
|
19
|
+
<div className="vk-skeleton" aria-hidden="true">
|
|
20
|
+
|
|
21
|
+
</div>
|
|
22
|
+
<div className="vk-skeleton" aria-hidden="true">
|
|
23
|
+
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div className="vk-skeleton" aria-hidden="true">
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
<div className="vk-search-wrap">
|
|
31
|
+
<div className="vk-skeleton" aria-hidden="true"> </div>
|
|
32
|
+
</div>
|
|
33
|
+
<div className="vk-content-grid">
|
|
34
|
+
<div>
|
|
35
|
+
<div className="vk-skeleton" aria-hidden="true"> </div>
|
|
36
|
+
<div className="vk-skeleton" aria-hidden="true"> </div>
|
|
37
|
+
<div className="vk-skeleton" aria-hidden="true"> </div>
|
|
38
|
+
</div>
|
|
39
|
+
<div>
|
|
40
|
+
<div className="vk-skeleton" aria-hidden="true"> </div>
|
|
41
|
+
<div className="vk-skeleton" aria-hidden="true"> </div>
|
|
42
|
+
<div className="vk-skeleton" aria-hidden="true"> </div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ---------------------------------------------- */
|
|
51
|
+
/* Alert component */
|
|
52
|
+
/* ---------------------------------------------- */
|
|
53
|
+
function Alert({
|
|
54
|
+
type,
|
|
55
|
+
message,
|
|
56
|
+
onDismiss,
|
|
57
|
+
}: {
|
|
58
|
+
type: "error" | "success";
|
|
59
|
+
message: string;
|
|
60
|
+
onDismiss?: () => void;
|
|
61
|
+
}) {
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
className={cn("vk-alert vk-slide-up", type === "error" ? "error" : "success")}
|
|
65
|
+
role="alert"
|
|
66
|
+
>
|
|
67
|
+
<span>{type === "error" ? "\u26A0" : "\u2713"}</span>
|
|
68
|
+
<span>{message}</span>
|
|
69
|
+
{onDismiss && (
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
onClick={onDismiss}
|
|
73
|
+
className="vk-alert-dismiss"
|
|
74
|
+
aria-label="Dismiss"
|
|
75
|
+
>
|
|
76
|
+
✕
|
|
77
|
+
</button>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* ---------------------------------------------- */
|
|
84
|
+
/* FAQ Accordion item */
|
|
85
|
+
/* ---------------------------------------------- */
|
|
86
|
+
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
|
87
|
+
const [open, setOpen] = useState(false);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="vk-faq-item">
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
onClick={() => setOpen(!open)}
|
|
94
|
+
className="vk-faq-question"
|
|
95
|
+
aria-expanded={open}
|
|
96
|
+
>
|
|
97
|
+
{question}
|
|
98
|
+
<span className={cn("vk-faq-chevron", open && "open")}>▼</span>
|
|
99
|
+
</button>
|
|
100
|
+
<div className={cn("vk-faq-body", open && "open")}>
|
|
101
|
+
<div>
|
|
102
|
+
<div className="vk-faq-answer">{answer}</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* ---------------------------------------------- */
|
|
110
|
+
/* Main page */
|
|
111
|
+
/* ---------------------------------------------- */
|
|
112
|
+
export default function SupportPage() {
|
|
113
|
+
const [templates, setTemplates] = useState<Template[]>([]);
|
|
114
|
+
const [articles, setArticles] = useState<Article[]>([]);
|
|
115
|
+
const [faqs, setFaqs] = useState<Faq[]>([]);
|
|
116
|
+
const [websiteName, setWebsiteName] = useState("Support");
|
|
117
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
118
|
+
const [error, setError] = useState("");
|
|
119
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
120
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
121
|
+
const [selectedArticle, setSelectedArticle] = useState<Article | null>(null);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
let isMounted = true;
|
|
125
|
+
const load = async () => {
|
|
126
|
+
setIsLoading(true);
|
|
127
|
+
setError("");
|
|
128
|
+
try {
|
|
129
|
+
const data = await fetchSupportInit();
|
|
130
|
+
if (!isMounted) return;
|
|
131
|
+
setTemplates(data.templates || []);
|
|
132
|
+
setArticles(data.articles || []);
|
|
133
|
+
setFaqs(data.faqs || []);
|
|
134
|
+
setWebsiteName(data.website?.name || "Support");
|
|
135
|
+
} catch (loadError) {
|
|
136
|
+
if (!isMounted) return;
|
|
137
|
+
setError(loadError instanceof Error ? loadError.message : "Unexpected error.");
|
|
138
|
+
} finally {
|
|
139
|
+
if (isMounted) setIsLoading(false);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
load();
|
|
144
|
+
return () => {
|
|
145
|
+
isMounted = false;
|
|
146
|
+
};
|
|
147
|
+
}, []);
|
|
148
|
+
|
|
149
|
+
/* Filtered articles & FAQs by search query */
|
|
150
|
+
const filteredArticles = useMemo(() => {
|
|
151
|
+
if (!searchQuery.trim()) return articles;
|
|
152
|
+
const q = searchQuery.toLowerCase();
|
|
153
|
+
return articles.filter(
|
|
154
|
+
(a) =>
|
|
155
|
+
a.title.toLowerCase().includes(q) ||
|
|
156
|
+
stripHtml(a.content).toLowerCase().includes(q),
|
|
157
|
+
);
|
|
158
|
+
}, [articles, searchQuery]);
|
|
159
|
+
|
|
160
|
+
const filteredFaqs = useMemo(() => {
|
|
161
|
+
if (!searchQuery.trim()) return faqs;
|
|
162
|
+
const q = searchQuery.toLowerCase();
|
|
163
|
+
return faqs.filter(
|
|
164
|
+
(f) =>
|
|
165
|
+
f.question.toLowerCase().includes(q) ||
|
|
166
|
+
f.answer.toLowerCase().includes(q),
|
|
167
|
+
);
|
|
168
|
+
}, [faqs, searchQuery]);
|
|
169
|
+
|
|
170
|
+
const hasContent = articles.length > 0 || faqs.length > 0;
|
|
171
|
+
const hasResults = filteredArticles.length > 0 || filteredFaqs.length > 0;
|
|
172
|
+
|
|
173
|
+
/* Loading state */
|
|
174
|
+
if (isLoading) {
|
|
175
|
+
return <HomeSkeleton />;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* Article viewer */
|
|
179
|
+
if (selectedArticle) {
|
|
180
|
+
return (
|
|
181
|
+
<div className="vk-shell">
|
|
182
|
+
<div className="vk-page vk-animate-in">
|
|
183
|
+
{/* Hero stays visible */}
|
|
184
|
+
<div className="vk-hero-row">
|
|
185
|
+
<div>
|
|
186
|
+
<h1 className="vk-hero-title">{websiteName}</h1>
|
|
187
|
+
<p className="vk-hero-subtitle">How can we help you today?</p>
|
|
188
|
+
</div>
|
|
189
|
+
{templates.length > 0 && (
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
className="vk-button primary pill"
|
|
193
|
+
onClick={() => setDialogOpen(true)}
|
|
194
|
+
>
|
|
195
|
+
💬 Contact Support
|
|
196
|
+
</button>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* Article content */}
|
|
201
|
+
<div className="vk-article-viewer">
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
onClick={() => setSelectedArticle(null)}
|
|
205
|
+
className="vk-back-button"
|
|
206
|
+
>
|
|
207
|
+
← Back to articles
|
|
208
|
+
</button>
|
|
209
|
+
|
|
210
|
+
<div className="vk-article-viewer-card">
|
|
211
|
+
<h2 className="vk-article-viewer-title">{selectedArticle.title}</h2>
|
|
212
|
+
<div
|
|
213
|
+
className="vk-article-viewer-content vk-message-html"
|
|
214
|
+
dangerouslySetInnerHTML={{
|
|
215
|
+
__html: sanitizeHtml(selectedArticle.content),
|
|
216
|
+
}}
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<TicketDialog
|
|
223
|
+
open={dialogOpen}
|
|
224
|
+
onClose={() => setDialogOpen(false)}
|
|
225
|
+
templates={templates}
|
|
226
|
+
/>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* Home view */
|
|
232
|
+
return (
|
|
233
|
+
<div className="vk-shell">
|
|
234
|
+
<div className="vk-page vk-animate-in">
|
|
235
|
+
{/* Row 1 - Hero */}
|
|
236
|
+
<div className="vk-hero-row">
|
|
237
|
+
<div>
|
|
238
|
+
<h1 className="vk-hero-title">{websiteName}</h1>
|
|
239
|
+
<p className="vk-hero-subtitle">How can we help you today?</p>
|
|
240
|
+
</div>
|
|
241
|
+
{templates.length > 0 && (
|
|
242
|
+
<div className="vk-hero-cta">
|
|
243
|
+
<span className="vk-hero-cta-hint">
|
|
244
|
+
Can't find what you're looking for?
|
|
245
|
+
</span>
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
className="vk-button primary pill"
|
|
249
|
+
onClick={() => setDialogOpen(true)}
|
|
250
|
+
>
|
|
251
|
+
💬 Contact Support
|
|
252
|
+
</button>
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
{/* Alerts */}
|
|
258
|
+
{error && (
|
|
259
|
+
<Alert type="error" message={error} onDismiss={() => setError("")} />
|
|
260
|
+
)}
|
|
261
|
+
|
|
262
|
+
{/* Row 2 - Search bar */}
|
|
263
|
+
{hasContent && (
|
|
264
|
+
<div className="vk-search-wrap">
|
|
265
|
+
<span className="vk-search-icon">🔍</span>
|
|
266
|
+
<input
|
|
267
|
+
type="text"
|
|
268
|
+
className="vk-search-input"
|
|
269
|
+
placeholder="Search articles and FAQs..."
|
|
270
|
+
value={searchQuery}
|
|
271
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
272
|
+
/>
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
275
|
+
|
|
276
|
+
{/* Row 3 - Split content */}
|
|
277
|
+
{hasResults && (
|
|
278
|
+
<div className="vk-content-grid">
|
|
279
|
+
{/* Left: Articles */}
|
|
280
|
+
{filteredArticles.length > 0 && (
|
|
281
|
+
<div>
|
|
282
|
+
<div className="vk-section-title-row">
|
|
283
|
+
<span className="vk-section-title-icon">📄</span>
|
|
284
|
+
<h2 className="vk-section-title">Popular Articles</h2>
|
|
285
|
+
</div>
|
|
286
|
+
<div className="vk-article-list">
|
|
287
|
+
{filteredArticles.map((article) => (
|
|
288
|
+
<button
|
|
289
|
+
key={article.id}
|
|
290
|
+
type="button"
|
|
291
|
+
className="vk-article-card"
|
|
292
|
+
onClick={() => setSelectedArticle(article)}
|
|
293
|
+
>
|
|
294
|
+
<div className="vk-article-icon" aria-hidden="true">
|
|
295
|
+
📄
|
|
296
|
+
</div>
|
|
297
|
+
<div>
|
|
298
|
+
<h3 className="vk-article-title">{article.title}</h3>
|
|
299
|
+
{article.content && (
|
|
300
|
+
<p className="vk-article-preview">
|
|
301
|
+
{stripHtml(article.content).substring(0, 150)}
|
|
302
|
+
</p>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
</button>
|
|
306
|
+
))}
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
)}
|
|
310
|
+
|
|
311
|
+
{/* Right: FAQs */}
|
|
312
|
+
{filteredFaqs.length > 0 && (
|
|
313
|
+
<div>
|
|
314
|
+
<div className="vk-section-title-row">
|
|
315
|
+
<span className="vk-section-title-icon">❔</span>
|
|
316
|
+
<h2 className="vk-section-title">Frequently Asked Questions</h2>
|
|
317
|
+
</div>
|
|
318
|
+
<div className="vk-faq-list">
|
|
319
|
+
{filteredFaqs.map((faq) => (
|
|
320
|
+
<FaqItem key={faq.id} question={faq.question} answer={faq.answer} />
|
|
321
|
+
))}
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
)}
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
327
|
+
|
|
328
|
+
{/* No search results */}
|
|
329
|
+
{searchQuery.trim() && !hasResults && (
|
|
330
|
+
<div className="vk-no-results">
|
|
331
|
+
No results found for “{searchQuery}”
|
|
332
|
+
</div>
|
|
333
|
+
)}
|
|
334
|
+
|
|
335
|
+
{/* Empty state - no articles and no FAQs at all */}
|
|
336
|
+
{!hasContent && !error && (
|
|
337
|
+
<div className="vk-cta-card">
|
|
338
|
+
<div className="vk-cta-icon">💬</div>
|
|
339
|
+
<p className="vk-cta-text">Need help? Our team is here for you.</p>
|
|
340
|
+
<button
|
|
341
|
+
type="button"
|
|
342
|
+
className="vk-button primary pill"
|
|
343
|
+
onClick={() => setDialogOpen(true)}
|
|
344
|
+
>
|
|
345
|
+
Contact Support
|
|
346
|
+
</button>
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
<TicketDialog
|
|
352
|
+
open={dialogOpen}
|
|
353
|
+
onClose={() => setDialogOpen(false)}
|
|
354
|
+
templates={templates}
|
|
355
|
+
/>
|
|
356
|
+
</div>
|
|
357
|
+
);
|
|
358
|
+
}
|