@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.
Files changed (71) hide show
  1. package/bin/create-vicket-support.js +429 -389
  2. package/package.json +1 -1
  3. package/templates/next/src/app/api/vicket/[...path]/route.ts +2 -55
  4. package/templates/next/src/app/components/vicket/ReplyForm.tsx +154 -0
  5. package/templates/next/src/app/components/vicket/SupportContent.tsx +298 -0
  6. package/templates/next/src/app/components/vicket/TicketDialog.tsx +3 -3
  7. package/templates/next/src/app/support/page.tsx +27 -353
  8. package/templates/next/src/app/ticket/page.tsx +110 -325
  9. package/templates/next/src/app/vicket.css +1325 -1325
  10. package/templates/nuxt/app/assets/css/vicket.css +1325 -1325
  11. package/templates/nuxt/app/components/VicketReplyForm.vue +154 -0
  12. package/templates/nuxt/app/components/VicketSupportContent.vue +255 -0
  13. package/templates/nuxt/app/components/VicketTicketDialog.vue +2 -2
  14. package/templates/nuxt/app/pages/support.vue +7 -293
  15. package/templates/nuxt/app/pages/ticket.vue +36 -178
  16. package/templates/nuxt/server/api/vicket/[...path].ts +2 -85
  17. package/templates/sveltekit/src/lib/vicket/ReplyForm.svelte +134 -0
  18. package/templates/sveltekit/src/lib/vicket/SupportContent.svelte +263 -0
  19. package/templates/sveltekit/src/lib/vicket/TicketDialog.svelte +457 -459
  20. package/templates/sveltekit/src/lib/vicket.css +1325 -1325
  21. package/templates/sveltekit/src/routes/api/vicket/[...path]/+server.ts +2 -76
  22. package/templates/sveltekit/src/routes/support/+page.server.ts +13 -0
  23. package/templates/sveltekit/src/routes/support/+page.svelte +3 -312
  24. package/templates/sveltekit/src/routes/ticket/+page.server.ts +19 -0
  25. package/templates/sveltekit/src/routes/ticket/+page.svelte +13 -188
  26. package/templates-tailwind/next/src/app/api/vicket/[...path]/route.ts +6 -0
  27. package/templates-tailwind/next/src/app/support/page.tsx +33 -3
  28. package/templates-tailwind/next/src/app/ticket/page.tsx +249 -6
  29. package/templates-tailwind/next/src/components/vicket/reply-form.tsx +113 -0
  30. package/templates-tailwind/next/src/components/vicket/support-content.tsx +265 -0
  31. package/templates-tailwind/next/src/components/vicket/ticket-dialog.tsx +2 -2
  32. package/templates-tailwind/nuxt/app/components/VicketReplyForm.vue +169 -0
  33. package/templates-tailwind/nuxt/app/components/{VicketSupportPage.vue → VicketSupportContent.vue} +275 -317
  34. package/templates-tailwind/nuxt/app/components/VicketTicketDialog.vue +3 -0
  35. package/templates-tailwind/nuxt/app/pages/support.vue +10 -1
  36. package/templates-tailwind/nuxt/app/pages/ticket.vue +298 -1
  37. package/templates-tailwind/nuxt/server/api/vicket/[...path].ts +2 -0
  38. package/templates-tailwind/sveltekit/src/lib/vicket/ReplyForm.svelte +127 -0
  39. package/templates-tailwind/sveltekit/src/lib/vicket/{SupportPage.svelte → SupportContent.svelte} +9 -71
  40. package/templates-tailwind/sveltekit/src/lib/vicket/TicketDialog.svelte +405 -406
  41. package/templates-tailwind/sveltekit/src/routes/api/vicket/[...path]/+server.ts +3 -0
  42. package/templates-tailwind/sveltekit/src/routes/support/+page.server.ts +13 -0
  43. package/templates-tailwind/sveltekit/src/routes/support/+page.svelte +4 -2
  44. package/templates-tailwind/sveltekit/src/routes/ticket/+page.server.ts +19 -0
  45. package/templates-tailwind/sveltekit/src/routes/ticket/+page.svelte +292 -2
  46. package/templates/next/src/app/utils/vicket/api.ts +0 -149
  47. package/templates/next/src/app/utils/vicket/types.ts +0 -85
  48. package/templates/next/src/app/utils/vicket/utils.ts +0 -49
  49. package/templates/nuxt/app/composables/useVicket.ts +0 -274
  50. package/templates/sveltekit/src/lib/vicket/api.ts +0 -162
  51. package/templates/sveltekit/src/lib/vicket/types.ts +0 -87
  52. package/templates/sveltekit/src/lib/vicket/utils.ts +0 -55
  53. package/templates-tailwind/next/src/app/api/vicket/init/route.ts +0 -24
  54. package/templates-tailwind/next/src/app/api/vicket/messages/route.ts +0 -36
  55. package/templates-tailwind/next/src/app/api/vicket/thread/route.ts +0 -27
  56. package/templates-tailwind/next/src/app/api/vicket/tickets/route.ts +0 -37
  57. package/templates-tailwind/next/src/components/vicket/support-page.tsx +0 -359
  58. package/templates-tailwind/next/src/components/vicket/ticket-page.tsx +0 -425
  59. package/templates-tailwind/next/src/lib/vicket.ts +0 -257
  60. package/templates-tailwind/nuxt/app/components/VicketTicketPage.vue +0 -449
  61. package/templates-tailwind/nuxt/app/composables/use-vicket.ts +0 -249
  62. package/templates-tailwind/nuxt/server/api/vicket/init.get.ts +0 -22
  63. package/templates-tailwind/nuxt/server/api/vicket/messages.post.ts +0 -56
  64. package/templates-tailwind/nuxt/server/api/vicket/thread.get.ts +0 -26
  65. package/templates-tailwind/nuxt/server/api/vicket/tickets.post.ts +0 -53
  66. package/templates-tailwind/sveltekit/src/lib/vicket/TicketPage.svelte +0 -465
  67. package/templates-tailwind/sveltekit/src/lib/vicket/index.ts +0 -257
  68. package/templates-tailwind/sveltekit/src/routes/api/vicket/init/+server.ts +0 -22
  69. package/templates-tailwind/sveltekit/src/routes/api/vicket/messages/+server.ts +0 -40
  70. package/templates-tailwind/sveltekit/src/routes/api/vicket/thread/+server.ts +0 -25
  71. package/templates-tailwind/sveltekit/src/routes/api/vicket/tickets/+server.ts +0 -37
@@ -0,0 +1,134 @@
1
+ <script lang="ts">
2
+ import { cn, sendReply } from "vicket";
3
+ import { invalidateAll } from "$app/navigation";
4
+
5
+ let { token }: { token: string } = $props();
6
+
7
+ let content = $state("");
8
+ let files = $state<File[]>([]);
9
+ let isSending = $state(false);
10
+ let error = $state("");
11
+ let success = $state("");
12
+
13
+ function removeFile(index: number) {
14
+ files = files.filter((_, i) => i !== index);
15
+ }
16
+
17
+ function onFileChange(event: Event) {
18
+ const input = event.target as HTMLInputElement;
19
+ const newFiles = Array.from(input.files || []);
20
+ files = [...files, ...newFiles];
21
+ input.value = "";
22
+ }
23
+
24
+ async function onSubmitReply(event: SubmitEvent) {
25
+ event.preventDefault();
26
+ error = "";
27
+ success = "";
28
+
29
+ if (!content.trim() && files.length === 0) {
30
+ error = "Reply content is required.";
31
+ return;
32
+ }
33
+
34
+ if (!token.trim()) {
35
+ error = "Missing ticket token.";
36
+ return;
37
+ }
38
+
39
+ isSending = true;
40
+ try {
41
+ await sendReply(token, content.trim(), files);
42
+ content = "";
43
+ files = [];
44
+ success = "Reply sent.";
45
+ await invalidateAll();
46
+ } catch (replyError) {
47
+ error = replyError instanceof Error ? replyError.message : "Unexpected error.";
48
+ } finally {
49
+ isSending = false;
50
+ }
51
+ }
52
+ </script>
53
+
54
+ <!-- Alerts -->
55
+ {#if error}
56
+ <div class={cn("vk-alert vk-slide-up", "error")} role="alert">
57
+ <span>&#9888;</span>
58
+ <span>{error}</span>
59
+ <button
60
+ type="button"
61
+ onclick={() => (error = "")}
62
+ class="vk-alert-dismiss"
63
+ aria-label="Dismiss"
64
+ >
65
+ &#10005;
66
+ </button>
67
+ </div>
68
+ {/if}
69
+ {#if success}
70
+ <div class={cn("vk-alert vk-slide-up", "success")} role="alert">
71
+ <span>&#10003;</span>
72
+ <span>{success}</span>
73
+ <button
74
+ type="button"
75
+ onclick={() => (success = "")}
76
+ class="vk-alert-dismiss"
77
+ aria-label="Dismiss"
78
+ >
79
+ &#10005;
80
+ </button>
81
+ </div>
82
+ {/if}
83
+
84
+ <!-- Compose area -->
85
+ <div class="vk-compose">
86
+ <form class="vk-stack" onsubmit={onSubmitReply}>
87
+ <textarea
88
+ class="vk-textarea"
89
+ bind:value={content}
90
+ placeholder="Write your reply..."
91
+ ></textarea>
92
+
93
+ <div class="vk-compose-row">
94
+ <!-- File input -->
95
+ <div class="vk-compose-files">
96
+ <label class="vk-browse-btn">
97
+ &#128206; Browse files
98
+ <input
99
+ type="file"
100
+ multiple
101
+ onchange={onFileChange}
102
+ />
103
+ </label>
104
+ {#each files as file, i}
105
+ <span class="vk-file-chip">
106
+ &#128206;
107
+ <span class="vk-file-chip-name">{file.name}</span>
108
+ <button
109
+ type="button"
110
+ onclick={() => removeFile(i)}
111
+ aria-label="Remove {file.name}"
112
+ >
113
+ &#10005;
114
+ </button>
115
+ </span>
116
+ {/each}
117
+ </div>
118
+
119
+ <!-- Send -->
120
+ <button
121
+ class="vk-button primary"
122
+ disabled={isSending}
123
+ type="submit"
124
+ >
125
+ {#if isSending}
126
+ <span class="vk-spinner"></span>
127
+ Sending...
128
+ {:else}
129
+ Send
130
+ {/if}
131
+ </button>
132
+ </div>
133
+ </form>
134
+ </div>
@@ -0,0 +1,263 @@
1
+ <script lang="ts">
2
+ import { cn, stripHtml, sanitizeHtml, type Template, type Article, type Faq } from "vicket";
3
+ import TicketDialog from "./TicketDialog.svelte";
4
+
5
+ let {
6
+ initData = null,
7
+ initError = '',
8
+ }: {
9
+ initData: { website?: { name?: string }; templates: Template[]; articles?: Article[]; faqs?: Faq[] } | null;
10
+ initError: string;
11
+ } = $props();
12
+
13
+ /* ---------------------------------------------- */
14
+ /* State */
15
+ /* ---------------------------------------------- */
16
+ let templates = $state<Template[]>(initData?.templates || []);
17
+ let articles = $state<Article[]>(initData?.articles || []);
18
+ let faqs = $state<Faq[]>(initData?.faqs || []);
19
+ let websiteName = $state(initData?.website?.name || "Support");
20
+ let error = $state(initError || "");
21
+ let searchQuery = $state("");
22
+ let dialogOpen = $state(false);
23
+ let selectedArticle = $state<Article | null>(null);
24
+
25
+ /* FAQ open state tracking */
26
+ let openFaqIds = $state<Set<string>>(new Set());
27
+
28
+ /* ---------------------------------------------- */
29
+ /* Derived */
30
+ /* ---------------------------------------------- */
31
+ let filteredArticles = $derived.by(() => {
32
+ if (!searchQuery.trim()) return articles;
33
+ const q = searchQuery.toLowerCase();
34
+ return articles.filter(
35
+ (a) => a.title.toLowerCase().includes(q) || stripHtml(a.content).toLowerCase().includes(q),
36
+ );
37
+ });
38
+
39
+ let filteredFaqs = $derived.by(() => {
40
+ if (!searchQuery.trim()) return faqs;
41
+ const q = searchQuery.toLowerCase();
42
+ return faqs.filter(
43
+ (f) => f.question.toLowerCase().includes(q) || f.answer.toLowerCase().includes(q),
44
+ );
45
+ });
46
+
47
+ let hasContent = $derived(articles.length > 0 || faqs.length > 0);
48
+ let hasResults = $derived(filteredArticles.length > 0 || filteredFaqs.length > 0);
49
+
50
+ /* ---------------------------------------------- */
51
+ /* Functions */
52
+ /* ---------------------------------------------- */
53
+ function toggleFaq(id: string) {
54
+ const next = new Set(openFaqIds);
55
+ if (next.has(id)) {
56
+ next.delete(id);
57
+ } else {
58
+ next.add(id);
59
+ }
60
+ openFaqIds = next;
61
+ }
62
+ </script>
63
+
64
+ <!-- --------------------------------------- -->
65
+ <!-- Article viewer -->
66
+ <!-- --------------------------------------- -->
67
+ {#if selectedArticle}
68
+ <div class="vk-shell">
69
+ <div class="vk-page vk-animate-in">
70
+ <!-- Hero stays visible -->
71
+ <div class="vk-hero-row">
72
+ <div>
73
+ <h1 class="vk-hero-title">{websiteName}</h1>
74
+ <p class="vk-hero-subtitle">How can we help you today?</p>
75
+ </div>
76
+ {#if templates.length > 0}
77
+ <button
78
+ type="button"
79
+ class="vk-button primary pill"
80
+ onclick={() => (dialogOpen = true)}
81
+ >
82
+ &#128172; Contact Support
83
+ </button>
84
+ {/if}
85
+ </div>
86
+
87
+ <!-- Article content -->
88
+ <div class="vk-article-viewer">
89
+ <button
90
+ type="button"
91
+ onclick={() => (selectedArticle = null)}
92
+ class="vk-back-button"
93
+ >
94
+ &#8592; Back to articles
95
+ </button>
96
+
97
+ <div class="vk-article-viewer-card">
98
+ <h2 class="vk-article-viewer-title">{selectedArticle.title}</h2>
99
+ <div class="vk-article-viewer-content vk-message-html">
100
+ {@html sanitizeHtml(selectedArticle.content)}
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <!-- Ticket dialog (also accessible from article view) -->
107
+ <TicketDialog open={dialogOpen} onclose={() => (dialogOpen = false)} templates={templates} />
108
+ </div>
109
+
110
+ <!-- --------------------------------------- -->
111
+ <!-- Home view -->
112
+ <!-- --------------------------------------- -->
113
+ {:else}
114
+ <div class="vk-shell">
115
+ <div class="vk-page vk-animate-in">
116
+ <!-- Row 1 - Hero -->
117
+ <div class="vk-hero-row">
118
+ <div>
119
+ <h1 class="vk-hero-title">{websiteName}</h1>
120
+ <p class="vk-hero-subtitle">How can we help you today?</p>
121
+ </div>
122
+ {#if templates.length > 0}
123
+ <div class="vk-hero-cta">
124
+ <span class="vk-hero-cta-hint">
125
+ Can't find what you're looking for?
126
+ </span>
127
+ <button
128
+ type="button"
129
+ class="vk-button primary pill"
130
+ onclick={() => (dialogOpen = true)}
131
+ >
132
+ &#128172; Contact Support
133
+ </button>
134
+ </div>
135
+ {/if}
136
+ </div>
137
+
138
+ <!-- Alerts -->
139
+ {#if error}
140
+ <div class={cn("vk-alert vk-slide-up", "error")} role="alert">
141
+ <span>&#9888;</span>
142
+ <span>{error}</span>
143
+ <button
144
+ type="button"
145
+ onclick={() => (error = "")}
146
+ class="vk-alert-dismiss"
147
+ aria-label="Dismiss"
148
+ >
149
+ &#10005;
150
+ </button>
151
+ </div>
152
+ {/if}
153
+
154
+ <!-- Row 2 - Search bar -->
155
+ {#if hasContent}
156
+ <div class="vk-search-wrap">
157
+ <span class="vk-search-icon">&#128269;</span>
158
+ <input
159
+ type="text"
160
+ class="vk-search-input"
161
+ placeholder="Search articles and FAQs..."
162
+ bind:value={searchQuery}
163
+ />
164
+ </div>
165
+ {/if}
166
+
167
+ <!-- Row 3 - Split content -->
168
+ {#if hasResults}
169
+ <div class="vk-content-grid">
170
+ <!-- Left: Articles -->
171
+ {#if filteredArticles.length > 0}
172
+ <div>
173
+ <div class="vk-section-title-row">
174
+ <span class="vk-section-title-icon">&#128196;</span>
175
+ <h2 class="vk-section-title">Popular Articles</h2>
176
+ </div>
177
+ <div class="vk-article-list">
178
+ {#each filteredArticles as article (article.id)}
179
+ <button
180
+ type="button"
181
+ class="vk-article-card"
182
+ onclick={() => (selectedArticle = article)}
183
+ >
184
+ <div class="vk-article-icon" aria-hidden="true">
185
+ &#128196;
186
+ </div>
187
+ <div>
188
+ <h3 class="vk-article-title">{article.title}</h3>
189
+ {#if article.content}
190
+ <p class="vk-article-preview">
191
+ {stripHtml(article.content).substring(0, 150)}
192
+ </p>
193
+ {/if}
194
+ </div>
195
+ </button>
196
+ {/each}
197
+ </div>
198
+ </div>
199
+ {/if}
200
+
201
+ <!-- Right: FAQs -->
202
+ {#if filteredFaqs.length > 0}
203
+ <div>
204
+ <div class="vk-section-title-row">
205
+ <span class="vk-section-title-icon">&#10068;</span>
206
+ <h2 class="vk-section-title">Frequently Asked Questions</h2>
207
+ </div>
208
+ <div class="vk-faq-list">
209
+ {#each filteredFaqs as faq (faq.id)}
210
+ <div class="vk-faq-item">
211
+ <button
212
+ type="button"
213
+ onclick={() => toggleFaq(faq.id)}
214
+ class="vk-faq-question"
215
+ aria-expanded={openFaqIds.has(faq.id)}
216
+ >
217
+ {faq.question}
218
+ <span class={cn("vk-faq-chevron", openFaqIds.has(faq.id) && "open")}>&#9660;</span>
219
+ </button>
220
+ <div class={cn("vk-faq-body", openFaqIds.has(faq.id) && "open")}>
221
+ <div>
222
+ <div class="vk-faq-answer">{faq.answer}</div>
223
+ </div>
224
+ </div>
225
+ </div>
226
+ {/each}
227
+ </div>
228
+ </div>
229
+ {/if}
230
+ </div>
231
+ {/if}
232
+
233
+ <!-- No search results -->
234
+ {#if searchQuery.trim() && !hasResults}
235
+ <div class="vk-no-results">
236
+ No results found for &ldquo;{searchQuery}&rdquo;
237
+ </div>
238
+ {/if}
239
+
240
+ <!-- Empty state - no articles and no FAQs at all -->
241
+ {#if !hasContent && !error}
242
+ <div class="vk-cta-card">
243
+ <div class="vk-cta-icon">&#128172;</div>
244
+ <p class="vk-cta-text">Need help? Our team is here for you.</p>
245
+ <button
246
+ type="button"
247
+ class="vk-button primary pill"
248
+ onclick={() => (dialogOpen = true)}
249
+ >
250
+ Contact Support
251
+ </button>
252
+ </div>
253
+ {/if}
254
+ </div>
255
+
256
+ <!-- Ticket dialog -->
257
+ <TicketDialog open={dialogOpen} onclose={() => (dialogOpen = false)} templates={templates} />
258
+ </div>
259
+ {/if}
260
+
261
+ <style>
262
+ @import '$lib/vicket.css';
263
+ </style>