@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,154 @@
1
+ <script setup lang="ts">
2
+ import { cn, sendReply } from "vicket";
3
+
4
+ /* ---------------------------------------------- */
5
+ /* Props & Emits */
6
+ /* ---------------------------------------------- */
7
+ const props = defineProps<{
8
+ token: string;
9
+ }>();
10
+
11
+ const emit = defineEmits<{
12
+ replied: [];
13
+ }>();
14
+
15
+ /* ---------------------------------------------- */
16
+ /* Reactive state */
17
+ /* ---------------------------------------------- */
18
+ const content = ref("");
19
+ const files = ref<File[]>([]);
20
+ const isSending = ref(false);
21
+ const error = ref("");
22
+ const success = ref("");
23
+
24
+ /* ---------------------------------------------- */
25
+ /* Helpers */
26
+ /* ---------------------------------------------- */
27
+ const removeFile = (index: number) => {
28
+ files.value = files.value.filter((_, i) => i !== index);
29
+ };
30
+
31
+ const onFileChange = (event: Event) => {
32
+ const input = event.target as HTMLInputElement;
33
+ const newFiles = Array.from(input.files || []);
34
+ files.value = [...files.value, ...newFiles];
35
+ input.value = "";
36
+ };
37
+
38
+ const submitReply = async () => {
39
+ error.value = "";
40
+ success.value = "";
41
+
42
+ if (!content.value.trim() && files.value.length === 0) {
43
+ error.value = "Reply content is required.";
44
+ return;
45
+ }
46
+
47
+ if (!props.token.trim()) {
48
+ error.value = "Missing ticket token.";
49
+ return;
50
+ }
51
+
52
+ isSending.value = true;
53
+ try {
54
+ await sendReply(props.token, content.value.trim(), files.value);
55
+ content.value = "";
56
+ files.value = [];
57
+ success.value = "Reply sent.";
58
+ emit("replied");
59
+ } catch (replyError) {
60
+ error.value = replyError instanceof Error ? replyError.message : "Unexpected error.";
61
+ } finally {
62
+ isSending.value = false;
63
+ }
64
+ };
65
+ </script>
66
+
67
+ <template>
68
+ <!-- Alerts -->
69
+ <div
70
+ v-if="error"
71
+ :class="cn('vk-alert vk-slide-up', 'error')"
72
+ role="alert"
73
+ >
74
+ <span>&#9888;</span>
75
+ <span>{{ error }}</span>
76
+ <button
77
+ type="button"
78
+ class="vk-alert-dismiss"
79
+ aria-label="Dismiss"
80
+ @click="error = ''"
81
+ >
82
+ &#10005;
83
+ </button>
84
+ </div>
85
+ <div
86
+ v-if="success"
87
+ :class="cn('vk-alert vk-slide-up', 'success')"
88
+ role="alert"
89
+ >
90
+ <span>&#10003;</span>
91
+ <span>{{ success }}</span>
92
+ <button
93
+ type="button"
94
+ class="vk-alert-dismiss"
95
+ aria-label="Dismiss"
96
+ @click="success = ''"
97
+ >
98
+ &#10005;
99
+ </button>
100
+ </div>
101
+
102
+ <!-- Compose area -->
103
+ <div class="vk-compose">
104
+ <form class="vk-stack" @submit.prevent="submitReply">
105
+ <textarea
106
+ v-model="content"
107
+ class="vk-textarea"
108
+ placeholder="Write your reply..."
109
+ />
110
+
111
+ <div class="vk-compose-row">
112
+ <!-- File input -->
113
+ <div class="vk-compose-files">
114
+ <label class="vk-browse-btn">
115
+ &#128206; Browse files
116
+ <input
117
+ type="file"
118
+ multiple
119
+ @change="onFileChange"
120
+ />
121
+ </label>
122
+ <span
123
+ v-for="(file, i) in files"
124
+ :key="`${file.name}-${i}`"
125
+ class="vk-file-chip"
126
+ >
127
+ &#128206;
128
+ <span class="vk-file-chip-name">{{ file.name }}</span>
129
+ <button
130
+ type="button"
131
+ :aria-label="`Remove ${file.name}`"
132
+ @click="removeFile(i)"
133
+ >
134
+ &#10005;
135
+ </button>
136
+ </span>
137
+ </div>
138
+
139
+ <!-- Send -->
140
+ <button
141
+ class="vk-button primary"
142
+ :disabled="isSending"
143
+ type="submit"
144
+ >
145
+ <template v-if="isSending">
146
+ <span class="vk-spinner" />
147
+ Sending...
148
+ </template>
149
+ <template v-else>Send</template>
150
+ </button>
151
+ </div>
152
+ </form>
153
+ </div>
154
+ </template>
@@ -0,0 +1,255 @@
1
+ <script setup lang="ts">
2
+ import type { Template, Article, Faq } from "vicket";
3
+ import { cn, stripHtml, sanitizeHtml } from "vicket";
4
+
5
+ /* ---------------------------------------------- */
6
+ /* Props */
7
+ /* ---------------------------------------------- */
8
+ const props = defineProps<{
9
+ initData?: { website?: { name?: string }; templates: Template[]; articles?: Article[]; faqs?: Faq[] } | null;
10
+ initError?: string;
11
+ }>();
12
+
13
+ /* ---------------------------------------------- */
14
+ /* Reactive state */
15
+ /* ---------------------------------------------- */
16
+ const templates = computed(() => props.initData?.templates || []);
17
+ const articles = computed(() => props.initData?.articles || []);
18
+ const faqs = computed(() => props.initData?.faqs || []);
19
+ const websiteName = computed(() => props.initData?.website?.name || "Support");
20
+ const error = ref(props.initError || "");
21
+ const searchQuery = ref("");
22
+ const dialogOpen = ref(false);
23
+ const selectedArticle = ref<Article | null>(null);
24
+
25
+ /* FAQ open state */
26
+ const openFaqIds = ref<Set<string>>(new Set());
27
+
28
+ /* ---------------------------------------------- */
29
+ /* Computed */
30
+ /* ---------------------------------------------- */
31
+ const filteredArticles = computed(() => {
32
+ if (!searchQuery.value.trim()) return articles.value;
33
+ const q = searchQuery.value.toLowerCase();
34
+ return articles.value.filter(
35
+ (a) =>
36
+ a.title.toLowerCase().includes(q) ||
37
+ stripHtml(a.content).toLowerCase().includes(q),
38
+ );
39
+ });
40
+
41
+ const filteredFaqs = computed(() => {
42
+ if (!searchQuery.value.trim()) return faqs.value;
43
+ const q = searchQuery.value.toLowerCase();
44
+ return faqs.value.filter(
45
+ (f) =>
46
+ f.question.toLowerCase().includes(q) ||
47
+ f.answer.toLowerCase().includes(q),
48
+ );
49
+ });
50
+
51
+ const hasContent = computed(() => articles.value.length > 0 || faqs.value.length > 0);
52
+ const hasResults = computed(() => filteredArticles.value.length > 0 || filteredFaqs.value.length > 0);
53
+
54
+ /* ---------------------------------------------- */
55
+ /* FAQ accordion */
56
+ /* ---------------------------------------------- */
57
+ const toggleFaq = (id: string) => {
58
+ const next = new Set(openFaqIds.value);
59
+ if (next.has(id)) {
60
+ next.delete(id);
61
+ } else {
62
+ next.add(id);
63
+ }
64
+ openFaqIds.value = next;
65
+ };
66
+
67
+ const isFaqOpen = (id: string) => openFaqIds.value.has(id);
68
+
69
+ /* ---------------------------------------------- */
70
+ /* Dialog helpers */
71
+ /* ---------------------------------------------- */
72
+ const openDialog = () => {
73
+ dialogOpen.value = true;
74
+ };
75
+ </script>
76
+
77
+ <template>
78
+ <!-- Article viewer -->
79
+ <div v-if="selectedArticle" class="vk-shell">
80
+ <div class="vk-page vk-animate-in">
81
+ <!-- Hero stays visible -->
82
+ <div class="vk-hero-row">
83
+ <div>
84
+ <h1 class="vk-hero-title">{{ websiteName }}</h1>
85
+ <p class="vk-hero-subtitle">How can we help you today?</p>
86
+ </div>
87
+ <button
88
+ v-if="templates.length > 0"
89
+ type="button"
90
+ class="vk-button primary pill"
91
+ @click="openDialog"
92
+ >
93
+ &#128172; Contact Support
94
+ </button>
95
+ </div>
96
+
97
+ <!-- Article content -->
98
+ <div class="vk-article-viewer">
99
+ <button
100
+ type="button"
101
+ class="vk-back-button"
102
+ @click="selectedArticle = null"
103
+ >
104
+ &#8592; Back to articles
105
+ </button>
106
+
107
+ <div class="vk-article-viewer-card">
108
+ <h2 class="vk-article-viewer-title">{{ selectedArticle.title }}</h2>
109
+ <div
110
+ class="vk-article-viewer-content vk-message-html"
111
+ v-html="sanitizeHtml(selectedArticle.content)"
112
+ />
113
+ </div>
114
+ </div>
115
+ </div>
116
+
117
+ <VicketTicketDialog
118
+ v-model="dialogOpen"
119
+ :templates="templates"
120
+ />
121
+ </div>
122
+
123
+ <!-- Home view -->
124
+ <div v-else class="vk-shell">
125
+ <div class="vk-page vk-animate-in">
126
+ <!-- Row 1 - Hero -->
127
+ <div class="vk-hero-row">
128
+ <div>
129
+ <h1 class="vk-hero-title">{{ websiteName }}</h1>
130
+ <p class="vk-hero-subtitle">How can we help you today?</p>
131
+ </div>
132
+ <div v-if="templates.length > 0" class="vk-hero-cta">
133
+ <span class="vk-hero-cta-hint">Can't find what you're looking for?</span>
134
+ <button
135
+ type="button"
136
+ class="vk-button primary pill"
137
+ @click="openDialog"
138
+ >
139
+ &#128172; Contact Support
140
+ </button>
141
+ </div>
142
+ </div>
143
+
144
+ <!-- Alerts -->
145
+ <div
146
+ v-if="error"
147
+ :class="cn('vk-alert vk-slide-up', 'error')"
148
+ role="alert"
149
+ >
150
+ <span>&#9888;</span>
151
+ <span>{{ error }}</span>
152
+ <button
153
+ type="button"
154
+ class="vk-alert-dismiss"
155
+ aria-label="Dismiss"
156
+ @click="error = ''"
157
+ >
158
+ &#10005;
159
+ </button>
160
+ </div>
161
+
162
+ <!-- Row 2 - Search bar -->
163
+ <div v-if="hasContent" class="vk-search-wrap">
164
+ <span class="vk-search-icon">&#128269;</span>
165
+ <input
166
+ type="text"
167
+ class="vk-search-input"
168
+ placeholder="Search articles and FAQs..."
169
+ v-model="searchQuery"
170
+ />
171
+ </div>
172
+
173
+ <!-- Row 3 - Split content -->
174
+ <div v-if="hasResults" class="vk-content-grid">
175
+ <!-- Left: Articles -->
176
+ <div v-if="filteredArticles.length > 0">
177
+ <div class="vk-section-title-row">
178
+ <span class="vk-section-title-icon">&#128196;</span>
179
+ <h2 class="vk-section-title">Popular Articles</h2>
180
+ </div>
181
+ <div class="vk-article-list">
182
+ <button
183
+ v-for="article in filteredArticles"
184
+ :key="article.id"
185
+ type="button"
186
+ class="vk-article-card"
187
+ @click="selectedArticle = article"
188
+ >
189
+ <div class="vk-article-icon" aria-hidden="true">&#128196;</div>
190
+ <div>
191
+ <h3 class="vk-article-title">{{ article.title }}</h3>
192
+ <p v-if="article.content" class="vk-article-preview">
193
+ {{ stripHtml(article.content).substring(0, 150) }}
194
+ </p>
195
+ </div>
196
+ </button>
197
+ </div>
198
+ </div>
199
+
200
+ <!-- Right: FAQs -->
201
+ <div v-if="filteredFaqs.length > 0">
202
+ <div class="vk-section-title-row">
203
+ <span class="vk-section-title-icon">&#10068;</span>
204
+ <h2 class="vk-section-title">Frequently Asked Questions</h2>
205
+ </div>
206
+ <div class="vk-faq-list">
207
+ <div
208
+ v-for="faq in filteredFaqs"
209
+ :key="faq.id"
210
+ class="vk-faq-item"
211
+ >
212
+ <button
213
+ type="button"
214
+ class="vk-faq-question"
215
+ :aria-expanded="isFaqOpen(faq.id)"
216
+ @click="toggleFaq(faq.id)"
217
+ >
218
+ {{ faq.question }}
219
+ <span :class="cn('vk-faq-chevron', isFaqOpen(faq.id) && 'open')">&#9660;</span>
220
+ </button>
221
+ <div :class="cn('vk-faq-body', isFaqOpen(faq.id) && 'open')">
222
+ <div>
223
+ <div class="vk-faq-answer">{{ faq.answer }}</div>
224
+ </div>
225
+ </div>
226
+ </div>
227
+ </div>
228
+ </div>
229
+ </div>
230
+
231
+ <!-- No search results -->
232
+ <div v-if="searchQuery.trim() && !hasResults" class="vk-no-results">
233
+ No results found for &ldquo;{{ searchQuery }}&rdquo;
234
+ </div>
235
+
236
+ <!-- Empty state - no articles and no FAQs at all -->
237
+ <div v-if="!hasContent && !error" class="vk-cta-card">
238
+ <div class="vk-cta-icon">&#128172;</div>
239
+ <p class="vk-cta-text">Need help? Our team is here for you.</p>
240
+ <button
241
+ type="button"
242
+ class="vk-button primary pill"
243
+ @click="openDialog"
244
+ >
245
+ Contact Support
246
+ </button>
247
+ </div>
248
+ </div>
249
+
250
+ <VicketTicketDialog
251
+ v-model="dialogOpen"
252
+ :templates="templates"
253
+ />
254
+ </div>
255
+ </template>
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
- import type { Template, FormValues } from "~/composables/useVicket";
3
- import { cn, initialFormValues, createTicket } from "~/composables/useVicket";
2
+ import type { Template, FormValues } from "vicket";
3
+ import { cn, initialFormValues, createTicket } from "vicket";
4
4
 
5
5
  /* ---------------------------------------------- */
6
6
  /* Props & Emits */