@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.
Files changed (55) hide show
  1. package/README.md +52 -0
  2. package/bin/create-vicket-support.js +389 -0
  3. package/package.json +18 -0
  4. package/templates/next/src/app/api/vicket/[...path]/route.ts +59 -0
  5. package/templates/next/src/app/components/vicket/TicketDialog.tsx +514 -0
  6. package/templates/next/src/app/support/page.tsx +358 -0
  7. package/templates/next/src/app/ticket/page.tsx +483 -0
  8. package/templates/next/src/app/utils/vicket/api.ts +149 -0
  9. package/templates/next/src/app/utils/vicket/types.ts +85 -0
  10. package/templates/next/src/app/utils/vicket/utils.ts +49 -0
  11. package/templates/next/src/app/vicket.css +1325 -0
  12. package/templates/nuxt/app/assets/css/vicket.css +1325 -0
  13. package/templates/nuxt/app/components/VicketTicketDialog.vue +499 -0
  14. package/templates/nuxt/app/composables/useVicket.ts +274 -0
  15. package/templates/nuxt/app/pages/support.vue +303 -0
  16. package/templates/nuxt/app/pages/ticket.vue +434 -0
  17. package/templates/nuxt/server/api/vicket/[...path].ts +85 -0
  18. package/templates/sveltekit/src/lib/vicket/TicketDialog.svelte +459 -0
  19. package/templates/sveltekit/src/lib/vicket/api.ts +162 -0
  20. package/templates/sveltekit/src/lib/vicket/types.ts +87 -0
  21. package/templates/sveltekit/src/lib/vicket/utils.ts +55 -0
  22. package/templates/sveltekit/src/lib/vicket.css +1325 -0
  23. package/templates/sveltekit/src/routes/api/vicket/[...path]/+server.ts +77 -0
  24. package/templates/sveltekit/src/routes/support/+page.svelte +316 -0
  25. package/templates/sveltekit/src/routes/ticket/+page.svelte +418 -0
  26. package/templates-tailwind/next/src/app/api/vicket/init/route.ts +24 -0
  27. package/templates-tailwind/next/src/app/api/vicket/messages/route.ts +36 -0
  28. package/templates-tailwind/next/src/app/api/vicket/thread/route.ts +27 -0
  29. package/templates-tailwind/next/src/app/api/vicket/tickets/route.ts +37 -0
  30. package/templates-tailwind/next/src/app/support/page.tsx +5 -0
  31. package/templates-tailwind/next/src/app/ticket/page.tsx +10 -0
  32. package/templates-tailwind/next/src/components/vicket/support-page.tsx +359 -0
  33. package/templates-tailwind/next/src/components/vicket/ticket-dialog.tsx +306 -0
  34. package/templates-tailwind/next/src/components/vicket/ticket-page.tsx +425 -0
  35. package/templates-tailwind/next/src/lib/vicket.ts +257 -0
  36. package/templates-tailwind/nuxt/app/components/VicketSupportPage.vue +317 -0
  37. package/templates-tailwind/nuxt/app/components/VicketTicketDialog.vue +444 -0
  38. package/templates-tailwind/nuxt/app/components/VicketTicketPage.vue +449 -0
  39. package/templates-tailwind/nuxt/app/composables/use-vicket.ts +249 -0
  40. package/templates-tailwind/nuxt/app/pages/support.vue +3 -0
  41. package/templates-tailwind/nuxt/app/pages/ticket.vue +3 -0
  42. package/templates-tailwind/nuxt/server/api/vicket/init.get.ts +22 -0
  43. package/templates-tailwind/nuxt/server/api/vicket/messages.post.ts +56 -0
  44. package/templates-tailwind/nuxt/server/api/vicket/thread.get.ts +26 -0
  45. package/templates-tailwind/nuxt/server/api/vicket/tickets.post.ts +53 -0
  46. package/templates-tailwind/sveltekit/src/lib/vicket/SupportPage.svelte +395 -0
  47. package/templates-tailwind/sveltekit/src/lib/vicket/TicketDialog.svelte +406 -0
  48. package/templates-tailwind/sveltekit/src/lib/vicket/TicketPage.svelte +465 -0
  49. package/templates-tailwind/sveltekit/src/lib/vicket/index.ts +257 -0
  50. package/templates-tailwind/sveltekit/src/routes/api/vicket/init/+server.ts +22 -0
  51. package/templates-tailwind/sveltekit/src/routes/api/vicket/messages/+server.ts +40 -0
  52. package/templates-tailwind/sveltekit/src/routes/api/vicket/thread/+server.ts +25 -0
  53. package/templates-tailwind/sveltekit/src/routes/api/vicket/tickets/+server.ts +37 -0
  54. package/templates-tailwind/sveltekit/src/routes/support/+page.svelte +5 -0
  55. package/templates-tailwind/sveltekit/src/routes/ticket/+page.svelte +5 -0
@@ -0,0 +1,77 @@
1
+ import { VICKET_API_URL, VICKET_API_KEY } from '$env/static/private';
2
+ import { json } from '@sveltejs/kit';
3
+ import type { RequestHandler } from './$types';
4
+
5
+ const baseUrl = (VICKET_API_URL || '').replace(/\/+$/, '');
6
+
7
+ export const GET: RequestHandler = async ({ params, url }) => {
8
+ if (!baseUrl || !VICKET_API_KEY) {
9
+ return json({ success: false, error: 'Server misconfigured: missing VICKET_API_URL or VICKET_API_KEY.' }, { status: 500 });
10
+ }
11
+
12
+ const path = params.path || '';
13
+ const query = url.searchParams.toString();
14
+ const target = `${baseUrl}/public/support/${path}${query ? `?${query}` : ''}`;
15
+
16
+ const response = await fetch(target, {
17
+ method: 'GET',
18
+ headers: {
19
+ 'X-Api-Key': VICKET_API_KEY,
20
+ 'Content-Type': 'application/json',
21
+ },
22
+ });
23
+
24
+ const body = await response.text();
25
+
26
+ return new Response(body, {
27
+ status: response.status,
28
+ headers: {
29
+ 'Content-Type': response.headers.get('Content-Type') || 'application/json',
30
+ },
31
+ });
32
+ };
33
+
34
+ export const POST: RequestHandler = async ({ params, url, request }) => {
35
+ if (!baseUrl || !VICKET_API_KEY) {
36
+ return json({ success: false, error: 'Server misconfigured: missing VICKET_API_URL or VICKET_API_KEY.' }, { status: 500 });
37
+ }
38
+
39
+ const path = params.path || '';
40
+ const query = url.searchParams.toString();
41
+ const target = `${baseUrl}/public/support/${path}${query ? `?${query}` : ''}`;
42
+
43
+ const contentType = request.headers.get('Content-Type') || '';
44
+ const isFormData = contentType.includes('multipart/form-data');
45
+
46
+ let response: Response;
47
+
48
+ if (isFormData) {
49
+ const formData = await request.formData();
50
+ response = await fetch(target, {
51
+ method: 'POST',
52
+ headers: {
53
+ 'X-Api-Key': VICKET_API_KEY,
54
+ },
55
+ body: formData,
56
+ });
57
+ } else {
58
+ const body = await request.text();
59
+ response = await fetch(target, {
60
+ method: 'POST',
61
+ headers: {
62
+ 'X-Api-Key': VICKET_API_KEY,
63
+ 'Content-Type': 'application/json',
64
+ },
65
+ body,
66
+ });
67
+ }
68
+
69
+ const responseBody = await response.text();
70
+
71
+ return new Response(responseBody, {
72
+ status: response.status,
73
+ headers: {
74
+ 'Content-Type': response.headers.get('Content-Type') || 'application/json',
75
+ },
76
+ });
77
+ };
@@ -0,0 +1,316 @@
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import type { Article, Faq, Template } from "$lib/vicket/types";
4
+ import { cn, stripHtml, sanitizeHtml } from "$lib/vicket/utils";
5
+ import { fetchSupportInit } from "$lib/vicket/api";
6
+ import TicketDialog from "$lib/vicket/TicketDialog.svelte";
7
+
8
+ /* ---------------------------------------------- */
9
+ /* State */
10
+ /* ---------------------------------------------- */
11
+ let templates = $state<Template[]>([]);
12
+ let articles = $state<Article[]>([]);
13
+ let faqs = $state<Faq[]>([]);
14
+ let websiteName = $state("Support");
15
+ let isLoading = $state(true);
16
+ let error = $state("");
17
+ let searchQuery = $state("");
18
+ let dialogOpen = $state(false);
19
+ let selectedArticle = $state<Article | null>(null);
20
+
21
+ /* FAQ open state tracking */
22
+ let openFaqIds = $state<Set<string>>(new Set());
23
+
24
+ /* ---------------------------------------------- */
25
+ /* Derived */
26
+ /* ---------------------------------------------- */
27
+ let filteredArticles = $derived.by(() => {
28
+ if (!searchQuery.trim()) return articles;
29
+ const q = searchQuery.toLowerCase();
30
+ return articles.filter(
31
+ (a) => a.title.toLowerCase().includes(q) || stripHtml(a.content).toLowerCase().includes(q),
32
+ );
33
+ });
34
+
35
+ let filteredFaqs = $derived.by(() => {
36
+ if (!searchQuery.trim()) return faqs;
37
+ const q = searchQuery.toLowerCase();
38
+ return faqs.filter(
39
+ (f) => f.question.toLowerCase().includes(q) || f.answer.toLowerCase().includes(q),
40
+ );
41
+ });
42
+
43
+ let hasContent = $derived(articles.length > 0 || faqs.length > 0);
44
+ let hasResults = $derived(filteredArticles.length > 0 || filteredFaqs.length > 0);
45
+
46
+ /* ---------------------------------------------- */
47
+ /* Functions */
48
+ /* ---------------------------------------------- */
49
+ function toggleFaq(id: string) {
50
+ const next = new Set(openFaqIds);
51
+ if (next.has(id)) {
52
+ next.delete(id);
53
+ } else {
54
+ next.add(id);
55
+ }
56
+ openFaqIds = next;
57
+ }
58
+
59
+ onMount(() => {
60
+ const load = async () => {
61
+ isLoading = true;
62
+ error = "";
63
+ try {
64
+ const data = await fetchSupportInit();
65
+ templates = data.templates || [];
66
+ articles = data.articles || [];
67
+ faqs = data.faqs || [];
68
+ websiteName = data.website?.name || "Support";
69
+ } catch (loadError) {
70
+ error = loadError instanceof Error ? loadError.message : "Unexpected error.";
71
+ } finally {
72
+ isLoading = false;
73
+ }
74
+ };
75
+
76
+ void load();
77
+ });
78
+ </script>
79
+
80
+ <!-- --------------------------------------- -->
81
+ <!-- Loading skeleton -->
82
+ <!-- --------------------------------------- -->
83
+ {#if isLoading}
84
+ <div class="vk-shell">
85
+ <div class="vk-page vk-animate-in">
86
+ <div class="vk-hero-row">
87
+ <div>
88
+ <div class="vk-skeleton" aria-hidden="true">
89
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
90
+ </div>
91
+ <div class="vk-skeleton" aria-hidden="true">
92
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
93
+ </div>
94
+ </div>
95
+ <div class="vk-skeleton" aria-hidden="true">
96
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
97
+ </div>
98
+ </div>
99
+ <div class="vk-search-wrap">
100
+ <div class="vk-skeleton" aria-hidden="true">&nbsp;</div>
101
+ </div>
102
+ <div class="vk-content-grid">
103
+ <div>
104
+ <div class="vk-skeleton" aria-hidden="true">&nbsp;</div>
105
+ <div class="vk-skeleton" aria-hidden="true">&nbsp;</div>
106
+ <div class="vk-skeleton" aria-hidden="true">&nbsp;</div>
107
+ </div>
108
+ <div>
109
+ <div class="vk-skeleton" aria-hidden="true">&nbsp;</div>
110
+ <div class="vk-skeleton" aria-hidden="true">&nbsp;</div>
111
+ <div class="vk-skeleton" aria-hidden="true">&nbsp;</div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+
117
+ <!-- --------------------------------------- -->
118
+ <!-- Article viewer -->
119
+ <!-- --------------------------------------- -->
120
+ {:else if selectedArticle}
121
+ <div class="vk-shell">
122
+ <div class="vk-page vk-animate-in">
123
+ <!-- Hero stays visible -->
124
+ <div class="vk-hero-row">
125
+ <div>
126
+ <h1 class="vk-hero-title">{websiteName}</h1>
127
+ <p class="vk-hero-subtitle">How can we help you today?</p>
128
+ </div>
129
+ {#if templates.length > 0}
130
+ <button
131
+ type="button"
132
+ class="vk-button primary pill"
133
+ onclick={() => (dialogOpen = true)}
134
+ >
135
+ &#128172; Contact Support
136
+ </button>
137
+ {/if}
138
+ </div>
139
+
140
+ <!-- Article content -->
141
+ <div class="vk-article-viewer">
142
+ <button
143
+ type="button"
144
+ onclick={() => (selectedArticle = null)}
145
+ class="vk-back-button"
146
+ >
147
+ &#8592; Back to articles
148
+ </button>
149
+
150
+ <div class="vk-article-viewer-card">
151
+ <h2 class="vk-article-viewer-title">{selectedArticle.title}</h2>
152
+ <div class="vk-article-viewer-content vk-message-html">
153
+ {@html sanitizeHtml(selectedArticle.content)}
154
+ </div>
155
+ </div>
156
+ </div>
157
+ </div>
158
+
159
+ <!-- Ticket dialog (also accessible from article view) -->
160
+ <TicketDialog open={dialogOpen} onclose={() => (dialogOpen = false)} templates={templates} />
161
+ </div>
162
+
163
+ <!-- --------------------------------------- -->
164
+ <!-- Home view -->
165
+ <!-- --------------------------------------- -->
166
+ {:else}
167
+ <div class="vk-shell">
168
+ <div class="vk-page vk-animate-in">
169
+ <!-- Row 1 - Hero -->
170
+ <div class="vk-hero-row">
171
+ <div>
172
+ <h1 class="vk-hero-title">{websiteName}</h1>
173
+ <p class="vk-hero-subtitle">How can we help you today?</p>
174
+ </div>
175
+ {#if templates.length > 0}
176
+ <div class="vk-hero-cta">
177
+ <span class="vk-hero-cta-hint">
178
+ Can't find what you're looking for?
179
+ </span>
180
+ <button
181
+ type="button"
182
+ class="vk-button primary pill"
183
+ onclick={() => (dialogOpen = true)}
184
+ >
185
+ &#128172; Contact Support
186
+ </button>
187
+ </div>
188
+ {/if}
189
+ </div>
190
+
191
+ <!-- Alerts -->
192
+ {#if error}
193
+ <div class={cn("vk-alert vk-slide-up", "error")} role="alert">
194
+ <span>&#9888;</span>
195
+ <span>{error}</span>
196
+ <button
197
+ type="button"
198
+ onclick={() => (error = "")}
199
+ class="vk-alert-dismiss"
200
+ aria-label="Dismiss"
201
+ >
202
+ &#10005;
203
+ </button>
204
+ </div>
205
+ {/if}
206
+
207
+ <!-- Row 2 - Search bar -->
208
+ {#if hasContent}
209
+ <div class="vk-search-wrap">
210
+ <span class="vk-search-icon">&#128269;</span>
211
+ <input
212
+ type="text"
213
+ class="vk-search-input"
214
+ placeholder="Search articles and FAQs..."
215
+ bind:value={searchQuery}
216
+ />
217
+ </div>
218
+ {/if}
219
+
220
+ <!-- Row 3 - Split content -->
221
+ {#if hasResults}
222
+ <div class="vk-content-grid">
223
+ <!-- Left: Articles -->
224
+ {#if filteredArticles.length > 0}
225
+ <div>
226
+ <div class="vk-section-title-row">
227
+ <span class="vk-section-title-icon">&#128196;</span>
228
+ <h2 class="vk-section-title">Popular Articles</h2>
229
+ </div>
230
+ <div class="vk-article-list">
231
+ {#each filteredArticles as article (article.id)}
232
+ <button
233
+ type="button"
234
+ class="vk-article-card"
235
+ onclick={() => (selectedArticle = article)}
236
+ >
237
+ <div class="vk-article-icon" aria-hidden="true">
238
+ &#128196;
239
+ </div>
240
+ <div>
241
+ <h3 class="vk-article-title">{article.title}</h3>
242
+ {#if article.content}
243
+ <p class="vk-article-preview">
244
+ {stripHtml(article.content).substring(0, 150)}
245
+ </p>
246
+ {/if}
247
+ </div>
248
+ </button>
249
+ {/each}
250
+ </div>
251
+ </div>
252
+ {/if}
253
+
254
+ <!-- Right: FAQs -->
255
+ {#if filteredFaqs.length > 0}
256
+ <div>
257
+ <div class="vk-section-title-row">
258
+ <span class="vk-section-title-icon">&#10068;</span>
259
+ <h2 class="vk-section-title">Frequently Asked Questions</h2>
260
+ </div>
261
+ <div class="vk-faq-list">
262
+ {#each filteredFaqs as faq (faq.id)}
263
+ <div class="vk-faq-item">
264
+ <button
265
+ type="button"
266
+ onclick={() => toggleFaq(faq.id)}
267
+ class="vk-faq-question"
268
+ aria-expanded={openFaqIds.has(faq.id)}
269
+ >
270
+ {faq.question}
271
+ <span class={cn("vk-faq-chevron", openFaqIds.has(faq.id) && "open")}>&#9660;</span>
272
+ </button>
273
+ <div class={cn("vk-faq-body", openFaqIds.has(faq.id) && "open")}>
274
+ <div>
275
+ <div class="vk-faq-answer">{faq.answer}</div>
276
+ </div>
277
+ </div>
278
+ </div>
279
+ {/each}
280
+ </div>
281
+ </div>
282
+ {/if}
283
+ </div>
284
+ {/if}
285
+
286
+ <!-- No search results -->
287
+ {#if searchQuery.trim() && !hasResults}
288
+ <div class="vk-no-results">
289
+ No results found for &ldquo;{searchQuery}&rdquo;
290
+ </div>
291
+ {/if}
292
+
293
+ <!-- Empty state - no articles and no FAQs at all -->
294
+ {#if !hasContent && !error}
295
+ <div class="vk-cta-card">
296
+ <div class="vk-cta-icon">&#128172;</div>
297
+ <p class="vk-cta-text">Need help? Our team is here for you.</p>
298
+ <button
299
+ type="button"
300
+ class="vk-button primary pill"
301
+ onclick={() => (dialogOpen = true)}
302
+ >
303
+ Contact Support
304
+ </button>
305
+ </div>
306
+ {/if}
307
+ </div>
308
+
309
+ <!-- Ticket dialog -->
310
+ <TicketDialog open={dialogOpen} onclose={() => (dialogOpen = false)} templates={templates} />
311
+ </div>
312
+ {/if}
313
+
314
+ <style>
315
+ @import '$lib/vicket.css';
316
+ </style>