@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
@@ -1,317 +1,275 @@
1
- <script setup lang="ts">
2
- /* ── Reactive state ─────────────────────────────── */
3
- const templates = ref<Template[]>([]);
4
- const articles = ref<Article[]>([]);
5
- const faqs = ref<Faq[]>([]);
6
- const websiteName = ref("Support");
7
- const isLoading = ref(true);
8
- const error = ref("");
9
- const searchQuery = ref("");
10
- const dialogOpen = ref(false);
11
- const selectedArticle = ref<Article | null>(null);
12
-
13
- /* FAQ accordion open state */
14
- const openFaqIds = ref<Set<string>>(new Set());
15
-
16
- /* ── Computed ───────────────────────────────────── */
17
- const filteredArticles = computed(() => {
18
- if (!searchQuery.value.trim()) return articles.value;
19
- const q = searchQuery.value.toLowerCase();
20
- return articles.value.filter(
21
- (a) =>
22
- a.title.toLowerCase().includes(q) ||
23
- stripHtml(a.content).toLowerCase().includes(q),
24
- );
25
- });
26
-
27
- const filteredFaqs = computed(() => {
28
- if (!searchQuery.value.trim()) return faqs.value;
29
- const q = searchQuery.value.toLowerCase();
30
- return faqs.value.filter(
31
- (f) =>
32
- f.question.toLowerCase().includes(q) ||
33
- f.answer.toLowerCase().includes(q),
34
- );
35
- });
36
-
37
- const hasContent = computed(() => articles.value.length > 0 || faqs.value.length > 0);
38
- const hasResults = computed(() => filteredArticles.value.length > 0 || filteredFaqs.value.length > 0);
39
-
40
- /* ── FAQ accordion ──────────────────────────────── */
41
- const toggleFaq = (id: string) => {
42
- const next = new Set(openFaqIds.value);
43
- if (next.has(id)) {
44
- next.delete(id);
45
- } else {
46
- next.add(id);
47
- }
48
- openFaqIds.value = next;
49
- };
50
-
51
- const isFaqOpen = (id: string) => openFaqIds.value.has(id);
52
-
53
- /* ── Data loading ───────────────────────────────── */
54
- onMounted(async () => {
55
- isLoading.value = true;
56
- error.value = "";
57
- try {
58
- const data = await fetchInit();
59
- templates.value = data.templates || [];
60
- articles.value = data.articles || [];
61
- faqs.value = data.faqs || [];
62
- websiteName.value = data.website?.name || "Support";
63
- } catch (loadError) {
64
- error.value = loadError instanceof Error ? loadError.message : "Unexpected error.";
65
- } finally {
66
- isLoading.value = false;
67
- }
68
- });
69
- </script>
70
-
71
- <template>
72
- <div class="min-h-screen bg-slate-50 font-[system-ui,-apple-system,sans-serif] antialiased">
73
- <!-- Skeleton loading state -->
74
- <div v-if="isLoading">
75
- <div class="mx-auto flex max-w-5xl flex-col gap-6 px-6 pt-14 md:flex-row md:items-center md:justify-between">
76
- <div class="flex-1">
77
- <div class="mb-3 h-9 w-48 animate-pulse rounded-lg bg-slate-200" />
78
- <div class="h-5 w-64 animate-pulse rounded-md bg-slate-200" />
79
- </div>
80
- <div class="h-10 w-40 animate-pulse rounded-full bg-slate-200" />
81
- </div>
82
- <div class="mx-auto mt-8 max-w-5xl px-6">
83
- <div class="h-11 w-full animate-pulse rounded-xl bg-slate-200" />
84
- </div>
85
- <div class="mx-auto mt-8 grid max-w-5xl grid-cols-1 gap-8 px-6 md:grid-cols-2">
86
- <div class="space-y-3">
87
- <div class="h-5 w-36 animate-pulse rounded bg-slate-200" />
88
- <div v-for="i in 3" :key="i" class="h-20 animate-pulse rounded-xl bg-slate-200" />
89
- </div>
90
- <div class="space-y-3">
91
- <div class="h-5 w-52 animate-pulse rounded bg-slate-200" />
92
- <div v-for="i in 3" :key="i" class="h-14 animate-pulse rounded-xl bg-slate-200" />
93
- </div>
94
- </div>
95
- </div>
96
-
97
- <!-- Article detail view -->
98
- <div v-else-if="selectedArticle" class="mx-auto max-w-5xl px-6">
99
- <div class="flex flex-col gap-4 pb-8 pt-14 md:flex-row md:items-center md:justify-between">
100
- <div>
101
- <h1 class="m-0 text-2xl font-bold tracking-tight text-slate-900 md:text-3xl">{{ websiteName }}</h1>
102
- <p class="mt-1.5 text-slate-500">How can we help you today?</p>
103
- </div>
104
- <button
105
- v-if="templates.length > 0"
106
- type="button"
107
- class="inline-flex shrink-0 items-center gap-2 !rounded-full border-none bg-blue-600 !px-7 !py-3 text-sm font-semibold text-white cursor-pointer transition-all hover:bg-blue-700 hover:-translate-y-px hover:shadow-lg active:translate-y-0"
108
- @click="dialogOpen = true"
109
- >
110
- <!-- IconMessageCircle -->
111
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" /></svg>
112
- Contact Support
113
- </button>
114
- </div>
115
-
116
- <div class="pb-16">
117
- <button
118
- type="button"
119
- class="-ml-2 mb-4 inline-flex cursor-pointer items-center gap-1.5 rounded-lg border-none bg-transparent px-2 py-1 text-sm font-medium text-slate-500 transition-colors hover:bg-white hover:text-slate-900"
120
- @click="selectedArticle = null"
121
- >
122
- <!-- IconArrowLeft -->
123
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg>
124
- Back to articles
125
- </button>
126
-
127
- <div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm md:p-8">
128
- <h2 class="m-0 text-xl font-bold tracking-tight text-slate-900 md:text-2xl">{{ selectedArticle.title }}</h2>
129
- <div
130
- class="vk-message-content mt-5 text-sm leading-relaxed text-slate-500"
131
- v-html="sanitizeHtml(selectedArticle.content)"
132
- />
133
- </div>
134
- </div>
135
-
136
- <VicketTicketDialog
137
- :open="dialogOpen"
138
- :templates="templates"
139
- @close="dialogOpen = false"
140
- />
141
- </div>
142
-
143
- <!-- Main hub view -->
144
- <div v-else class="mx-auto max-w-5xl px-6">
145
- <div class="flex flex-col gap-4 pb-8 pt-14 md:flex-row md:items-center md:justify-between">
146
- <div>
147
- <h1 class="m-0 text-2xl font-bold tracking-tight text-slate-900 md:text-3xl">{{ websiteName }}</h1>
148
- <p class="mt-1.5 text-slate-500">How can we help you today?</p>
149
- </div>
150
- <div v-if="templates.length > 0" class="flex items-center gap-3">
151
- <span class="hidden text-sm text-slate-500 md:block">Can't find what you're looking for?</span>
152
- <button
153
- type="button"
154
- class="inline-flex shrink-0 items-center gap-2 !rounded-full border-none bg-blue-600 !px-7 !py-3 text-sm font-semibold text-white cursor-pointer transition-all hover:bg-blue-700 hover:-translate-y-px hover:shadow-lg active:translate-y-0"
155
- @click="dialogOpen = true"
156
- >
157
- <!-- IconMessageCircle -->
158
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" /></svg>
159
- Contact Support
160
- </button>
161
- </div>
162
- </div>
163
-
164
- <!-- Error alert -->
165
- <div v-if="error" class="pb-4">
166
- <div
167
- class="flex items-start gap-3 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-900"
168
- role="alert"
169
- >
170
- <span class="mt-0.5 shrink-0">
171
- <!-- IconAlert -->
172
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" /><path d="M12 9v4" /><path d="M12 17h.01" /></svg>
173
- </span>
174
- <span class="flex-1">{{ error }}</span>
175
- <button
176
- type="button"
177
- class="shrink-0 cursor-pointer border-none bg-transparent p-0 opacity-50 transition-opacity hover:opacity-100"
178
- aria-label="Dismiss"
179
- @click="error = ''"
180
- >
181
- <!-- IconX -->
182
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
183
- </button>
184
- </div>
185
- </div>
186
-
187
- <!-- Search bar -->
188
- <div v-if="hasContent" class="pb-8">
189
- <div class="relative">
190
- <span class="pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-500">
191
- <!-- IconSearch -->
192
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>
193
- </span>
194
- <input
195
- type="text"
196
- class="w-full rounded-xl border border-slate-200 bg-white py-3 pl-11 pr-4 text-sm text-slate-900 shadow-sm transition-all duration-150 placeholder:text-slate-500/60 focus:border-blue-600 focus:outline-none focus:ring-3 focus:ring-blue-600/10"
197
- placeholder="Search articles and FAQs..."
198
- v-model="searchQuery"
199
- />
200
- </div>
201
- </div>
202
-
203
- <!-- Content grid -->
204
- <div v-if="hasResults" class="grid grid-cols-1 gap-10 pb-16 md:grid-cols-2 md:gap-12">
205
- <!-- Articles -->
206
- <div v-if="filteredArticles.length > 0">
207
- <div class="mb-4 flex items-center gap-2">
208
- <span class="text-blue-600">
209
- <!-- IconArticle 18 -->
210
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" /><path d="M14 2v6h6" /><path d="M16 13H8" /><path d="M16 17H8" /><path d="M10 9H8" /></svg>
211
- </span>
212
- <h2 class="m-0 text-base font-bold tracking-tight text-slate-900">Popular Articles</h2>
213
- </div>
214
- <div class="space-y-1">
215
- <button
216
- v-for="article in filteredArticles"
217
- :key="article.id"
218
- type="button"
219
- class="group flex w-full cursor-pointer items-start gap-3 rounded-xl border border-transparent bg-transparent p-3 text-left transition-all duration-150 hover:border-slate-200 hover:bg-white hover:shadow-sm"
220
- @click="selectedArticle = article"
221
- >
222
- <div class="flex h-9 w-9 min-w-9 items-center justify-center rounded-lg bg-blue-50 text-blue-600">
223
- <!-- IconArticle 16 -->
224
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" /><path d="M14 2v6h6" /><path d="M16 13H8" /><path d="M16 17H8" /><path d="M10 9H8" /></svg>
225
- </div>
226
- <div class="min-w-0 flex-1">
227
- <h3 class="m-0 text-sm font-semibold leading-snug text-slate-900 transition-colors group-hover:text-blue-600">{{ article.title }}</h3>
228
- <p v-if="article.content" class="mt-0.5 line-clamp-2 text-xs leading-normal text-slate-500">
229
- {{ stripHtml(article.content).substring(0, 150) }}
230
- </p>
231
- </div>
232
- </button>
233
- </div>
234
- </div>
235
-
236
- <!-- FAQs -->
237
- <div v-if="filteredFaqs.length > 0">
238
- <div class="mb-4 flex items-center gap-2">
239
- <span class="text-blue-600">
240
- <!-- IconHelpCircle -->
241
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" /><path d="M12 17h.01" /></svg>
242
- </span>
243
- <h2 class="m-0 text-base font-bold tracking-tight text-slate-900">Frequently Asked Questions</h2>
244
- </div>
245
- <div class="space-y-2">
246
- <div
247
- v-for="faq in filteredFaqs"
248
- :key="faq.id"
249
- class="rounded-xl border border-slate-200 bg-white/50 transition-colors hover:bg-white/80"
250
- >
251
- <button
252
- type="button"
253
- :class="cn(
254
- 'flex w-full cursor-pointer items-center justify-between gap-3 border-none bg-transparent px-4 py-3.5 text-left text-sm font-semibold text-slate-900',
255
- isFaqOpen(faq.id) && 'text-blue-600',
256
- )"
257
- :aria-expanded="isFaqOpen(faq.id)"
258
- @click="toggleFaq(faq.id)"
259
- >
260
- {{ faq.question }}
261
- <span :class="cn('shrink-0 text-slate-500 transition-transform duration-200', isFaqOpen(faq.id) && 'rotate-180')">
262
- <!-- IconChevronDown -->
263
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
264
- </span>
265
- </button>
266
- <div :class="cn('grid grid-rows-[0fr] transition-[grid-template-rows] duration-250', isFaqOpen(faq.id) && 'grid-rows-[1fr]')">
267
- <div class="overflow-hidden">
268
- <div class="whitespace-pre-wrap px-4 pb-4 text-sm leading-relaxed text-slate-500">
269
- {{ faq.answer }}
270
- </div>
271
- </div>
272
- </div>
273
- </div>
274
- </div>
275
- </div>
276
- </div>
277
-
278
- <!-- No search results -->
279
- <div v-if="searchQuery.trim() && !hasResults" class="pb-16 text-center">
280
- <p class="text-sm text-slate-500">No results found for &ldquo;{{ searchQuery }}&rdquo;</p>
281
- </div>
282
-
283
- <!-- Empty state -->
284
- <div v-if="!hasContent && !error" class="pb-16 pt-4 text-center">
285
- <div class="mx-auto max-w-sm rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
286
- <div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-blue-50 text-blue-600">
287
- <!-- IconMessageCircle 22 -->
288
- <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" /></svg>
289
- </div>
290
- <p class="m-0 text-sm font-medium text-slate-900">Need help? Our team is here for you.</p>
291
- <button
292
- type="button"
293
- class="mt-5 inline-flex items-center gap-2 !rounded-full border-none bg-blue-600 !px-7 !py-3 text-sm font-semibold text-white cursor-pointer transition-all hover:bg-blue-700 hover:-translate-y-px hover:shadow-lg active:translate-y-0"
294
- @click="dialogOpen = true"
295
- >
296
- Contact Support
297
- </button>
298
- </div>
299
- </div>
300
-
301
- <VicketTicketDialog
302
- :open="dialogOpen"
303
- :templates="templates"
304
- @close="dialogOpen = false"
305
- />
306
- </div>
307
- </div>
308
- </template>
309
-
310
- <style>
311
- .vk-message-content p { margin-bottom: 0.5rem; }
312
- .vk-message-content p:last-child { margin-bottom: 0; }
313
- .vk-message-content a { color: #2563eb; text-decoration: underline; }
314
- .vk-message-content ul, .vk-message-content ol { margin: 0.25rem 0; padding-left: 1.5rem; }
315
- .vk-message-content pre { overflow-x: auto; border-radius: 6px; background: #f8fafc; padding: 0.75rem; font-size: 0.75rem; }
316
- .vk-message-content code { font-family: monospace; font-size: 0.85em; }
317
- </style>
1
+ <script setup lang="ts">
2
+ import type { Template, Article, Faq } from "vicket";
3
+ import { cn, stripHtml, sanitizeHtml } from "vicket";
4
+
5
+ /* ── Props ─────────────────────────────────────── */
6
+ const props = defineProps<{
7
+ initData?: { website?: { name?: string }; templates: Template[]; articles?: Article[]; faqs?: Faq[] } | null;
8
+ initError?: string;
9
+ }>();
10
+
11
+ /* ── Reactive state ─────────────────────────────── */
12
+ const templates = ref<Template[]>(props.initData?.templates || []);
13
+ const articles = ref<Article[]>(props.initData?.articles || []);
14
+ const faqs = ref<Faq[]>(props.initData?.faqs || []);
15
+ const websiteName = ref(props.initData?.website?.name || "Support");
16
+ const error = ref(props.initError || "");
17
+ const searchQuery = ref("");
18
+ const dialogOpen = ref(false);
19
+ const selectedArticle = ref<Article | null>(null);
20
+
21
+ /* FAQ accordion open state */
22
+ const openFaqIds = ref<Set<string>>(new Set());
23
+
24
+ /* ── Computed ───────────────────────────────────── */
25
+ const filteredArticles = computed(() => {
26
+ if (!searchQuery.value.trim()) return articles.value;
27
+ const q = searchQuery.value.toLowerCase();
28
+ return articles.value.filter(
29
+ (a) =>
30
+ a.title.toLowerCase().includes(q) ||
31
+ stripHtml(a.content).toLowerCase().includes(q),
32
+ );
33
+ });
34
+
35
+ const filteredFaqs = computed(() => {
36
+ if (!searchQuery.value.trim()) return faqs.value;
37
+ const q = searchQuery.value.toLowerCase();
38
+ return faqs.value.filter(
39
+ (f) =>
40
+ f.question.toLowerCase().includes(q) ||
41
+ f.answer.toLowerCase().includes(q),
42
+ );
43
+ });
44
+
45
+ const hasContent = computed(() => articles.value.length > 0 || faqs.value.length > 0);
46
+ const hasResults = computed(() => filteredArticles.value.length > 0 || filteredFaqs.value.length > 0);
47
+
48
+ /* ── FAQ accordion ──────────────────────────────── */
49
+ const toggleFaq = (id: string) => {
50
+ const next = new Set(openFaqIds.value);
51
+ if (next.has(id)) {
52
+ next.delete(id);
53
+ } else {
54
+ next.add(id);
55
+ }
56
+ openFaqIds.value = next;
57
+ };
58
+
59
+ const isFaqOpen = (id: string) => openFaqIds.value.has(id);
60
+ </script>
61
+
62
+ <template>
63
+ <div class="min-h-screen bg-slate-50 font-[system-ui,-apple-system,sans-serif] antialiased">
64
+ <!-- Article detail view -->
65
+ <div v-if="selectedArticle" class="mx-auto max-w-5xl px-6">
66
+ <div class="flex flex-col gap-4 pb-8 pt-14 md:flex-row md:items-center md:justify-between">
67
+ <div>
68
+ <h1 class="m-0 text-2xl font-bold tracking-tight text-slate-900 md:text-3xl">{{ websiteName }}</h1>
69
+ <p class="mt-1.5 text-slate-500">How can we help you today?</p>
70
+ </div>
71
+ <button
72
+ v-if="templates.length > 0"
73
+ type="button"
74
+ class="inline-flex shrink-0 items-center gap-2 !rounded-full border-none bg-blue-600 !px-7 !py-3 text-sm font-semibold text-white cursor-pointer transition-all hover:bg-blue-700 hover:-translate-y-px hover:shadow-lg active:translate-y-0"
75
+ @click="dialogOpen = true"
76
+ >
77
+ <!-- IconMessageCircle -->
78
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" /></svg>
79
+ Contact Support
80
+ </button>
81
+ </div>
82
+
83
+ <div class="pb-16">
84
+ <button
85
+ type="button"
86
+ class="-ml-2 mb-4 inline-flex cursor-pointer items-center gap-1.5 rounded-lg border-none bg-transparent px-2 py-1 text-sm font-medium text-slate-500 transition-colors hover:bg-white hover:text-slate-900"
87
+ @click="selectedArticle = null"
88
+ >
89
+ <!-- IconArrowLeft -->
90
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg>
91
+ Back to articles
92
+ </button>
93
+
94
+ <div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm md:p-8">
95
+ <h2 class="m-0 text-xl font-bold tracking-tight text-slate-900 md:text-2xl">{{ selectedArticle.title }}</h2>
96
+ <div
97
+ class="mt-5 text-sm leading-relaxed text-slate-500 [&_a]:text-blue-600 [&_a]:underline [&_code]:font-mono [&_code]:text-[0.85em] [&_ol]:my-1 [&_ol]:pl-6 [&_p:last-child]:mb-0 [&_p]:mb-2 [&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-slate-50 [&_pre]:p-3 [&_pre]:text-xs [&_ul]:my-1 [&_ul]:pl-6"
98
+ v-html="sanitizeHtml(selectedArticle.content)"
99
+ />
100
+ </div>
101
+ </div>
102
+
103
+ <VicketTicketDialog
104
+ :open="dialogOpen"
105
+ :templates="templates"
106
+ @close="dialogOpen = false"
107
+ />
108
+ </div>
109
+
110
+ <!-- Main hub view -->
111
+ <div v-else class="mx-auto max-w-5xl px-6">
112
+ <div class="flex flex-col gap-4 pb-8 pt-14 md:flex-row md:items-center md:justify-between">
113
+ <div>
114
+ <h1 class="m-0 text-2xl font-bold tracking-tight text-slate-900 md:text-3xl">{{ websiteName }}</h1>
115
+ <p class="mt-1.5 text-slate-500">How can we help you today?</p>
116
+ </div>
117
+ <div v-if="templates.length > 0" class="flex items-center gap-3">
118
+ <span class="hidden text-sm text-slate-500 md:block">Can't find what you're looking for?</span>
119
+ <button
120
+ type="button"
121
+ class="inline-flex shrink-0 items-center gap-2 !rounded-full border-none bg-blue-600 !px-7 !py-3 text-sm font-semibold text-white cursor-pointer transition-all hover:bg-blue-700 hover:-translate-y-px hover:shadow-lg active:translate-y-0"
122
+ @click="dialogOpen = true"
123
+ >
124
+ <!-- IconMessageCircle -->
125
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" /></svg>
126
+ Contact Support
127
+ </button>
128
+ </div>
129
+ </div>
130
+
131
+ <!-- Error alert -->
132
+ <div v-if="error" class="pb-4">
133
+ <div
134
+ class="flex items-start gap-3 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-900"
135
+ role="alert"
136
+ >
137
+ <span class="mt-0.5 shrink-0">
138
+ <!-- IconAlert -->
139
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" /><path d="M12 9v4" /><path d="M12 17h.01" /></svg>
140
+ </span>
141
+ <span class="flex-1">{{ error }}</span>
142
+ <button
143
+ type="button"
144
+ class="shrink-0 cursor-pointer border-none bg-transparent p-0 opacity-50 transition-opacity hover:opacity-100"
145
+ aria-label="Dismiss"
146
+ @click="error = ''"
147
+ >
148
+ <!-- IconX -->
149
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
150
+ </button>
151
+ </div>
152
+ </div>
153
+
154
+ <!-- Search bar -->
155
+ <div v-if="hasContent" class="pb-8">
156
+ <div class="relative">
157
+ <span class="pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-500">
158
+ <!-- IconSearch -->
159
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>
160
+ </span>
161
+ <input
162
+ type="text"
163
+ class="w-full rounded-xl border border-slate-200 bg-white py-3 pl-11 pr-4 text-sm text-slate-900 shadow-sm transition-all duration-150 placeholder:text-slate-500/60 focus:border-blue-600 focus:outline-none focus:ring-3 focus:ring-blue-600/10"
164
+ placeholder="Search articles and FAQs..."
165
+ v-model="searchQuery"
166
+ />
167
+ </div>
168
+ </div>
169
+
170
+ <!-- Content grid -->
171
+ <div v-if="hasResults" class="grid grid-cols-1 gap-10 pb-16 md:grid-cols-2 md:gap-12">
172
+ <!-- Articles -->
173
+ <div v-if="filteredArticles.length > 0">
174
+ <div class="mb-4 flex items-center gap-2">
175
+ <span class="text-blue-600">
176
+ <!-- IconArticle 18 -->
177
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" /><path d="M14 2v6h6" /><path d="M16 13H8" /><path d="M16 17H8" /><path d="M10 9H8" /></svg>
178
+ </span>
179
+ <h2 class="m-0 text-base font-bold tracking-tight text-slate-900">Popular Articles</h2>
180
+ </div>
181
+ <div class="space-y-1">
182
+ <button
183
+ v-for="article in filteredArticles"
184
+ :key="article.id"
185
+ type="button"
186
+ class="group flex w-full cursor-pointer items-start gap-3 rounded-xl border border-transparent bg-transparent p-3 text-left transition-all duration-150 hover:border-slate-200 hover:bg-white hover:shadow-sm"
187
+ @click="selectedArticle = article"
188
+ >
189
+ <div class="flex h-9 w-9 min-w-9 items-center justify-center rounded-lg bg-blue-50 text-blue-600">
190
+ <!-- IconArticle 16 -->
191
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" /><path d="M14 2v6h6" /><path d="M16 13H8" /><path d="M16 17H8" /><path d="M10 9H8" /></svg>
192
+ </div>
193
+ <div class="min-w-0 flex-1">
194
+ <h3 class="m-0 text-sm font-semibold leading-snug text-slate-900 transition-colors group-hover:text-blue-600">{{ article.title }}</h3>
195
+ <p v-if="article.content" class="mt-0.5 line-clamp-2 text-xs leading-normal text-slate-500">
196
+ {{ stripHtml(article.content).substring(0, 150) }}
197
+ </p>
198
+ </div>
199
+ </button>
200
+ </div>
201
+ </div>
202
+
203
+ <!-- FAQs -->
204
+ <div v-if="filteredFaqs.length > 0">
205
+ <div class="mb-4 flex items-center gap-2">
206
+ <span class="text-blue-600">
207
+ <!-- IconHelpCircle -->
208
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" /><path d="M12 17h.01" /></svg>
209
+ </span>
210
+ <h2 class="m-0 text-base font-bold tracking-tight text-slate-900">Frequently Asked Questions</h2>
211
+ </div>
212
+ <div class="space-y-2">
213
+ <div
214
+ v-for="faq in filteredFaqs"
215
+ :key="faq.id"
216
+ class="rounded-xl border border-slate-200 bg-white/50 transition-colors hover:bg-white/80"
217
+ >
218
+ <button
219
+ type="button"
220
+ :class="cn(
221
+ 'flex w-full cursor-pointer items-center justify-between gap-3 border-none bg-transparent px-4 py-3.5 text-left text-sm font-semibold text-slate-900',
222
+ isFaqOpen(faq.id) && 'text-blue-600',
223
+ )"
224
+ :aria-expanded="isFaqOpen(faq.id)"
225
+ @click="toggleFaq(faq.id)"
226
+ >
227
+ {{ faq.question }}
228
+ <span :class="cn('shrink-0 text-slate-500 transition-transform duration-200', isFaqOpen(faq.id) && 'rotate-180')">
229
+ <!-- IconChevronDown -->
230
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
231
+ </span>
232
+ </button>
233
+ <div :class="cn('grid grid-rows-[0fr] transition-[grid-template-rows] duration-250', isFaqOpen(faq.id) && 'grid-rows-[1fr]')">
234
+ <div class="overflow-hidden">
235
+ <div class="whitespace-pre-wrap px-4 pb-4 text-sm leading-relaxed text-slate-500">
236
+ {{ faq.answer }}
237
+ </div>
238
+ </div>
239
+ </div>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ </div>
244
+
245
+ <!-- No search results -->
246
+ <div v-if="searchQuery.trim() && !hasResults" class="pb-16 text-center">
247
+ <p class="text-sm text-slate-500">No results found for &ldquo;{{ searchQuery }}&rdquo;</p>
248
+ </div>
249
+
250
+ <!-- Empty state -->
251
+ <div v-if="!hasContent && !error" class="pb-16 pt-4 text-center">
252
+ <div class="mx-auto max-w-sm rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
253
+ <div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-blue-50 text-blue-600">
254
+ <!-- IconMessageCircle 22 -->
255
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" /></svg>
256
+ </div>
257
+ <p class="m-0 text-sm font-medium text-slate-900">Need help? Our team is here for you.</p>
258
+ <button
259
+ type="button"
260
+ class="mt-5 inline-flex items-center gap-2 !rounded-full border-none bg-blue-600 !px-7 !py-3 text-sm font-semibold text-white cursor-pointer transition-all hover:bg-blue-700 hover:-translate-y-px hover:shadow-lg active:translate-y-0"
261
+ @click="dialogOpen = true"
262
+ >
263
+ Contact Support
264
+ </button>
265
+ </div>
266
+ </div>
267
+
268
+ <VicketTicketDialog
269
+ :open="dialogOpen"
270
+ :templates="templates"
271
+ @close="dialogOpen = false"
272
+ />
273
+ </div>
274
+ </div>
275
+ </template>