@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,395 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Template, Article, Faq } from "$lib/vicket";
|
|
3
|
+
import { fetchInit, sanitizeHtml, stripHtml, cn } from "$lib/vicket";
|
|
4
|
+
import TicketDialog from "./TicketDialog.svelte";
|
|
5
|
+
|
|
6
|
+
/* ---------------------------------------------- */
|
|
7
|
+
/* State */
|
|
8
|
+
/* ---------------------------------------------- */
|
|
9
|
+
let templates = $state<Template[]>([]);
|
|
10
|
+
let articles = $state<Article[]>([]);
|
|
11
|
+
let faqs = $state<Faq[]>([]);
|
|
12
|
+
let websiteName = $state("Support");
|
|
13
|
+
let isLoading = $state(true);
|
|
14
|
+
let error = $state("");
|
|
15
|
+
let searchQuery = $state("");
|
|
16
|
+
let dialogOpen = $state(false);
|
|
17
|
+
let selectedArticle = $state<Article | null>(null);
|
|
18
|
+
|
|
19
|
+
/* FAQ open state tracking */
|
|
20
|
+
let openFaqIds = $state<Set<string>>(new Set());
|
|
21
|
+
|
|
22
|
+
/* ---------------------------------------------- */
|
|
23
|
+
/* Derived */
|
|
24
|
+
/* ---------------------------------------------- */
|
|
25
|
+
let filteredArticles = $derived.by(() => {
|
|
26
|
+
if (!searchQuery.trim()) return articles;
|
|
27
|
+
const q = searchQuery.toLowerCase();
|
|
28
|
+
return articles.filter(
|
|
29
|
+
(a) => a.title.toLowerCase().includes(q) || stripHtml(a.content).toLowerCase().includes(q),
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
let filteredFaqs = $derived.by(() => {
|
|
34
|
+
if (!searchQuery.trim()) return faqs;
|
|
35
|
+
const q = searchQuery.toLowerCase();
|
|
36
|
+
return faqs.filter(
|
|
37
|
+
(f) => f.question.toLowerCase().includes(q) || f.answer.toLowerCase().includes(q),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
let hasContent = $derived(articles.length > 0 || faqs.length > 0);
|
|
42
|
+
let hasResults = $derived(filteredArticles.length > 0 || filteredFaqs.length > 0);
|
|
43
|
+
|
|
44
|
+
/* ---------------------------------------------- */
|
|
45
|
+
/* Functions */
|
|
46
|
+
/* ---------------------------------------------- */
|
|
47
|
+
function toggleFaq(id: string) {
|
|
48
|
+
const next = new Set(openFaqIds);
|
|
49
|
+
if (next.has(id)) {
|
|
50
|
+
next.delete(id);
|
|
51
|
+
} else {
|
|
52
|
+
next.add(id);
|
|
53
|
+
}
|
|
54
|
+
openFaqIds = next;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ---------------------------------------------- */
|
|
58
|
+
/* Load data on mount */
|
|
59
|
+
/* ---------------------------------------------- */
|
|
60
|
+
$effect(() => {
|
|
61
|
+
let isMounted = true;
|
|
62
|
+
const load = async () => {
|
|
63
|
+
isLoading = true;
|
|
64
|
+
error = "";
|
|
65
|
+
try {
|
|
66
|
+
const data = await fetchInit();
|
|
67
|
+
if (!isMounted) return;
|
|
68
|
+
templates = data.templates || [];
|
|
69
|
+
articles = data.articles || [];
|
|
70
|
+
faqs = data.faqs || [];
|
|
71
|
+
websiteName = data.website?.name || "Support";
|
|
72
|
+
} catch (loadError) {
|
|
73
|
+
if (!isMounted) return;
|
|
74
|
+
error = loadError instanceof Error ? loadError.message : "Unexpected error.";
|
|
75
|
+
} finally {
|
|
76
|
+
if (isMounted) isLoading = false;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
load();
|
|
80
|
+
return () => { isMounted = false; };
|
|
81
|
+
});
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<!-- --------------------------------------- -->
|
|
85
|
+
<!-- Loading skeleton -->
|
|
86
|
+
<!-- --------------------------------------- -->
|
|
87
|
+
{#if isLoading}
|
|
88
|
+
<div class="min-h-screen bg-slate-50 font-[system-ui,-apple-system,sans-serif] antialiased">
|
|
89
|
+
<div class="mx-auto max-w-5xl px-6 py-16">
|
|
90
|
+
<!-- Header skeleton -->
|
|
91
|
+
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
92
|
+
<div>
|
|
93
|
+
<div class="h-8 w-40 animate-pulse rounded-lg bg-slate-200" aria-hidden="true"></div>
|
|
94
|
+
<div class="mt-2 h-5 w-64 animate-pulse rounded-lg bg-slate-200" aria-hidden="true"></div>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="h-12 w-44 animate-pulse rounded-full bg-slate-200" aria-hidden="true"></div>
|
|
97
|
+
</div>
|
|
98
|
+
<!-- Search skeleton -->
|
|
99
|
+
<div class="mt-10">
|
|
100
|
+
<div class="h-12 w-full animate-pulse rounded-xl bg-slate-200" aria-hidden="true"></div>
|
|
101
|
+
</div>
|
|
102
|
+
<!-- Content skeleton -->
|
|
103
|
+
<div class="mt-10 grid gap-10 md:grid-cols-2">
|
|
104
|
+
<div class="space-y-3">
|
|
105
|
+
<div class="h-5 w-36 animate-pulse rounded-lg bg-slate-200" aria-hidden="true"></div>
|
|
106
|
+
<div class="h-16 w-full animate-pulse rounded-xl bg-slate-200" aria-hidden="true"></div>
|
|
107
|
+
<div class="h-16 w-full animate-pulse rounded-xl bg-slate-200" aria-hidden="true"></div>
|
|
108
|
+
<div class="h-16 w-full animate-pulse rounded-xl bg-slate-200" aria-hidden="true"></div>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="space-y-3">
|
|
111
|
+
<div class="h-5 w-52 animate-pulse rounded-lg bg-slate-200" aria-hidden="true"></div>
|
|
112
|
+
<div class="h-14 w-full animate-pulse rounded-xl bg-slate-200" aria-hidden="true"></div>
|
|
113
|
+
<div class="h-14 w-full animate-pulse rounded-xl bg-slate-200" aria-hidden="true"></div>
|
|
114
|
+
<div class="h-14 w-full animate-pulse rounded-xl bg-slate-200" aria-hidden="true"></div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<!-- --------------------------------------- -->
|
|
121
|
+
<!-- Article viewer -->
|
|
122
|
+
<!-- --------------------------------------- -->
|
|
123
|
+
{:else if selectedArticle}
|
|
124
|
+
<div class="min-h-screen bg-slate-50 font-[system-ui,-apple-system,sans-serif] antialiased">
|
|
125
|
+
<div class="mx-auto max-w-5xl px-6 py-16">
|
|
126
|
+
<!-- Hero stays visible -->
|
|
127
|
+
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
128
|
+
<div>
|
|
129
|
+
<h1 class="m-0 text-2xl font-bold tracking-tight text-slate-900 md:text-3xl">{websiteName}</h1>
|
|
130
|
+
<p class="mt-1 text-sm text-slate-500 md:text-base">How can we help you today?</p>
|
|
131
|
+
</div>
|
|
132
|
+
{#if templates.length > 0}
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
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"
|
|
136
|
+
onclick={() => (dialogOpen = true)}
|
|
137
|
+
>
|
|
138
|
+
<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>
|
|
139
|
+
Contact Support
|
|
140
|
+
</button>
|
|
141
|
+
{/if}
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<!-- Article content -->
|
|
145
|
+
<div class="mt-8">
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
onclick={() => (selectedArticle = null)}
|
|
149
|
+
class="mb-4 inline-flex cursor-pointer items-center gap-1.5 rounded-lg border-none bg-transparent px-2 py-1.5 text-sm font-medium text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-900"
|
|
150
|
+
>
|
|
151
|
+
<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>
|
|
152
|
+
Back to articles
|
|
153
|
+
</button>
|
|
154
|
+
|
|
155
|
+
<div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm md:p-8">
|
|
156
|
+
<h2 class="m-0 text-xl font-bold text-slate-900">{selectedArticle.title}</h2>
|
|
157
|
+
<div class="vk-message-content mt-4 text-sm leading-relaxed text-slate-700">
|
|
158
|
+
{@html sanitizeHtml(selectedArticle.content)}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{#if templates.length > 0}
|
|
163
|
+
<div class="mt-6 text-center">
|
|
164
|
+
<p class="text-sm text-slate-500">Still need help?</p>
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
class="mt-2 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"
|
|
168
|
+
onclick={() => (dialogOpen = true)}
|
|
169
|
+
>
|
|
170
|
+
<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>
|
|
171
|
+
Contact Support
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
{/if}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<!-- Ticket dialog (also accessible from article view) -->
|
|
179
|
+
<TicketDialog open={dialogOpen} onclose={() => (dialogOpen = false)} templates={templates} />
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<!-- --------------------------------------- -->
|
|
183
|
+
<!-- Home view -->
|
|
184
|
+
<!-- --------------------------------------- -->
|
|
185
|
+
{:else}
|
|
186
|
+
<div class="min-h-screen bg-slate-50 font-[system-ui,-apple-system,sans-serif] antialiased">
|
|
187
|
+
<div class="mx-auto max-w-5xl px-6 py-16">
|
|
188
|
+
<!-- Row 1 - Hero -->
|
|
189
|
+
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
190
|
+
<div>
|
|
191
|
+
<h1 class="m-0 text-2xl font-bold tracking-tight text-slate-900 md:text-3xl">{websiteName}</h1>
|
|
192
|
+
<p class="mt-1 text-sm text-slate-500 md:text-base">How can we help you today?</p>
|
|
193
|
+
</div>
|
|
194
|
+
{#if templates.length > 0}
|
|
195
|
+
<div class="flex flex-col items-end gap-1.5">
|
|
196
|
+
<span class="hidden text-xs text-slate-500 sm:block">
|
|
197
|
+
Can't find what you're looking for?
|
|
198
|
+
</span>
|
|
199
|
+
<button
|
|
200
|
+
type="button"
|
|
201
|
+
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"
|
|
202
|
+
onclick={() => (dialogOpen = true)}
|
|
203
|
+
>
|
|
204
|
+
<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>
|
|
205
|
+
Contact Support
|
|
206
|
+
</button>
|
|
207
|
+
</div>
|
|
208
|
+
{/if}
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<!-- Alerts -->
|
|
212
|
+
{#if error}
|
|
213
|
+
<div class="mt-6 flex items-start gap-3 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-900" role="alert">
|
|
214
|
+
<span class="mt-0.5 shrink-0">
|
|
215
|
+
<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>
|
|
216
|
+
</span>
|
|
217
|
+
<span class="flex-1">{error}</span>
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
onclick={() => (error = "")}
|
|
221
|
+
class="shrink-0 cursor-pointer border-none bg-transparent p-0 opacity-50 transition-opacity hover:opacity-100"
|
|
222
|
+
aria-label="Dismiss"
|
|
223
|
+
>
|
|
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="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
{/if}
|
|
228
|
+
|
|
229
|
+
<!-- Row 2 - Search bar -->
|
|
230
|
+
{#if hasContent}
|
|
231
|
+
<div class="relative mt-10">
|
|
232
|
+
<span class="pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400">
|
|
233
|
+
<svg width="16" height="16" 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>
|
|
234
|
+
</span>
|
|
235
|
+
<input
|
|
236
|
+
type="text"
|
|
237
|
+
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"
|
|
238
|
+
placeholder="Search articles and FAQs..."
|
|
239
|
+
bind:value={searchQuery}
|
|
240
|
+
/>
|
|
241
|
+
</div>
|
|
242
|
+
{/if}
|
|
243
|
+
|
|
244
|
+
<!-- Row 3 - Split content -->
|
|
245
|
+
{#if hasResults}
|
|
246
|
+
<div class="mt-10 grid gap-10 md:grid-cols-2">
|
|
247
|
+
<!-- Left: Articles -->
|
|
248
|
+
{#if filteredArticles.length > 0}
|
|
249
|
+
<div>
|
|
250
|
+
<div class="mb-4 flex items-center gap-2">
|
|
251
|
+
<span class="text-slate-400">
|
|
252
|
+
<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>
|
|
253
|
+
</span>
|
|
254
|
+
<h2 class="m-0 text-sm font-semibold uppercase tracking-wider text-slate-500">Popular Articles</h2>
|
|
255
|
+
</div>
|
|
256
|
+
<div class="space-y-1">
|
|
257
|
+
{#each filteredArticles as article (article.id)}
|
|
258
|
+
<button
|
|
259
|
+
type="button"
|
|
260
|
+
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"
|
|
261
|
+
onclick={() => (selectedArticle = article)}
|
|
262
|
+
>
|
|
263
|
+
<span class="mt-0.5 shrink-0 text-slate-400 transition-colors group-hover:text-blue-600">
|
|
264
|
+
<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>
|
|
265
|
+
</span>
|
|
266
|
+
<div class="min-w-0">
|
|
267
|
+
<h3 class="m-0 text-sm font-semibold text-slate-900 transition-colors group-hover:text-blue-600">{article.title}</h3>
|
|
268
|
+
{#if article.content}
|
|
269
|
+
<p class="m-0 mt-0.5 line-clamp-2 text-xs text-slate-500">
|
|
270
|
+
{stripHtml(article.content).substring(0, 150)}
|
|
271
|
+
</p>
|
|
272
|
+
{/if}
|
|
273
|
+
</div>
|
|
274
|
+
</button>
|
|
275
|
+
{/each}
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
{/if}
|
|
279
|
+
|
|
280
|
+
<!-- Right: FAQs -->
|
|
281
|
+
{#if filteredFaqs.length > 0}
|
|
282
|
+
<div>
|
|
283
|
+
<div class="mb-4 flex items-center gap-2">
|
|
284
|
+
<span class="text-slate-400">
|
|
285
|
+
<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>
|
|
286
|
+
</span>
|
|
287
|
+
<h2 class="m-0 text-sm font-semibold uppercase tracking-wider text-slate-500">Frequently Asked Questions</h2>
|
|
288
|
+
</div>
|
|
289
|
+
<div class="space-y-2">
|
|
290
|
+
{#each filteredFaqs as faq (faq.id)}
|
|
291
|
+
<div class="rounded-xl border border-slate-200 bg-white/50 transition-colors hover:bg-white/80">
|
|
292
|
+
<button
|
|
293
|
+
type="button"
|
|
294
|
+
onclick={() => toggleFaq(faq.id)}
|
|
295
|
+
class="flex w-full cursor-pointer items-center justify-between gap-3 border-none bg-transparent p-4 text-left text-sm font-semibold text-slate-900"
|
|
296
|
+
aria-expanded={openFaqIds.has(faq.id)}
|
|
297
|
+
>
|
|
298
|
+
{faq.question}
|
|
299
|
+
<span class={cn("shrink-0 text-slate-400 transition-transform duration-200", openFaqIds.has(faq.id) && "rotate-180")}>
|
|
300
|
+
<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>
|
|
301
|
+
</span>
|
|
302
|
+
</button>
|
|
303
|
+
{#if openFaqIds.has(faq.id)}
|
|
304
|
+
<div class="border-t border-slate-100 px-4 pb-4 pt-2">
|
|
305
|
+
<p class="m-0 text-sm leading-relaxed text-slate-600">{faq.answer}</p>
|
|
306
|
+
</div>
|
|
307
|
+
{/if}
|
|
308
|
+
</div>
|
|
309
|
+
{/each}
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
{/if}
|
|
313
|
+
</div>
|
|
314
|
+
{/if}
|
|
315
|
+
|
|
316
|
+
<!-- No search results -->
|
|
317
|
+
{#if searchQuery.trim() && !hasResults}
|
|
318
|
+
<div class="mt-16 text-center">
|
|
319
|
+
<p class="text-sm text-slate-500">No results found for “{searchQuery}”</p>
|
|
320
|
+
</div>
|
|
321
|
+
{/if}
|
|
322
|
+
|
|
323
|
+
<!-- Empty state - no articles and no FAQs at all -->
|
|
324
|
+
{#if !hasContent && !error}
|
|
325
|
+
<div class="mt-16 rounded-2xl border border-slate-200 bg-white p-10 text-center shadow-sm">
|
|
326
|
+
<div class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-blue-50 text-blue-600">
|
|
327
|
+
<svg width="24" height="24" 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>
|
|
328
|
+
</div>
|
|
329
|
+
<p class="text-sm text-slate-500">Need help? Our team is here for you.</p>
|
|
330
|
+
<button
|
|
331
|
+
type="button"
|
|
332
|
+
class="mt-4 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"
|
|
333
|
+
onclick={() => (dialogOpen = true)}
|
|
334
|
+
>
|
|
335
|
+
Contact Support
|
|
336
|
+
</button>
|
|
337
|
+
</div>
|
|
338
|
+
{/if}
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<!-- Ticket dialog -->
|
|
342
|
+
<TicketDialog open={dialogOpen} onclose={() => (dialogOpen = false)} templates={templates} />
|
|
343
|
+
</div>
|
|
344
|
+
{/if}
|
|
345
|
+
|
|
346
|
+
<style>
|
|
347
|
+
:global(.vk-message-content h1,
|
|
348
|
+
.vk-message-content h2,
|
|
349
|
+
.vk-message-content h3,
|
|
350
|
+
.vk-message-content h4,
|
|
351
|
+
.vk-message-content h5,
|
|
352
|
+
.vk-message-content h6) {
|
|
353
|
+
margin: 1em 0 0.5em;
|
|
354
|
+
line-height: 1.4;
|
|
355
|
+
}
|
|
356
|
+
:global(.vk-message-content p) {
|
|
357
|
+
margin: 0.5em 0;
|
|
358
|
+
}
|
|
359
|
+
:global(.vk-message-content ul,
|
|
360
|
+
.vk-message-content ol) {
|
|
361
|
+
margin: 0.5em 0;
|
|
362
|
+
padding-left: 1.5em;
|
|
363
|
+
}
|
|
364
|
+
:global(.vk-message-content a) {
|
|
365
|
+
color: #2563eb;
|
|
366
|
+
text-decoration: underline;
|
|
367
|
+
}
|
|
368
|
+
:global(.vk-message-content img) {
|
|
369
|
+
max-width: 100%;
|
|
370
|
+
border-radius: 0.5rem;
|
|
371
|
+
}
|
|
372
|
+
:global(.vk-message-content pre) {
|
|
373
|
+
background: #f1f5f9;
|
|
374
|
+
padding: 0.75em 1em;
|
|
375
|
+
border-radius: 0.5rem;
|
|
376
|
+
overflow-x: auto;
|
|
377
|
+
font-size: 0.85em;
|
|
378
|
+
}
|
|
379
|
+
:global(.vk-message-content code) {
|
|
380
|
+
background: #f1f5f9;
|
|
381
|
+
padding: 0.15em 0.35em;
|
|
382
|
+
border-radius: 0.25rem;
|
|
383
|
+
font-size: 0.9em;
|
|
384
|
+
}
|
|
385
|
+
:global(.vk-message-content pre code) {
|
|
386
|
+
background: none;
|
|
387
|
+
padding: 0;
|
|
388
|
+
}
|
|
389
|
+
:global(.vk-message-content blockquote) {
|
|
390
|
+
border-left: 3px solid #cbd5e1;
|
|
391
|
+
margin: 0.5em 0;
|
|
392
|
+
padding-left: 1em;
|
|
393
|
+
color: #64748b;
|
|
394
|
+
}
|
|
395
|
+
</style>
|