@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,37 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
-
3
- const VICKET_API_URL = (process.env.VICKET_API_URL || "").replace(/\/+$/, "");
4
- const VICKET_API_KEY = process.env.VICKET_API_KEY || "";
5
-
6
- export async function POST(req: NextRequest) {
7
- if (!VICKET_API_URL || !VICKET_API_KEY) {
8
- return NextResponse.json(
9
- { error: "Missing VICKET_API_URL or VICKET_API_KEY server environment variables." },
10
- { status: 500 },
11
- );
12
- }
13
-
14
- const contentType = req.headers.get("content-type") || "";
15
- const headers: HeadersInit = { "X-Api-Key": VICKET_API_KEY };
16
- let body: BodyInit;
17
-
18
- if (contentType.includes("multipart/form-data")) {
19
- body = await req.blob();
20
- headers["Content-Type"] = contentType;
21
- } else {
22
- body = await req.text();
23
- headers["Content-Type"] = "application/json";
24
- }
25
-
26
- const upstream = await fetch(`${VICKET_API_URL}/public/support/tickets`, {
27
- method: "POST",
28
- headers,
29
- body,
30
- });
31
-
32
- const responseBody = await upstream.arrayBuffer();
33
- return new NextResponse(responseBody, {
34
- status: upstream.status,
35
- headers: { "Content-Type": upstream.headers.get("Content-Type") || "application/json" },
36
- });
37
- }
@@ -1,359 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useMemo, useState } from "react";
4
- import type { Template, Article, Faq } from "@/lib/vicket";
5
- import { fetchInit, sanitizeHtml, stripHtml, cn } from "@/lib/vicket";
6
- import TicketDialog from "./ticket-dialog";
7
-
8
- /* ── Custom hook ─────────────────────────────────── */
9
-
10
- function useVicketSupport() {
11
- const [templates, setTemplates] = useState<Template[]>([]);
12
- const [articles, setArticles] = useState<Article[]>([]);
13
- const [faqs, setFaqs] = useState<Faq[]>([]);
14
- const [websiteName, setWebsiteName] = useState("Support");
15
- const [isLoading, setIsLoading] = useState(true);
16
- const [error, setError] = useState("");
17
- const [searchQuery, setSearchQuery] = useState("");
18
- const [dialogOpen, setDialogOpen] = useState(false);
19
- const [selectedArticle, setSelectedArticle] = useState<Article | null>(null);
20
-
21
- useEffect(() => {
22
- let isMounted = true;
23
- const load = async () => {
24
- setIsLoading(true);
25
- setError("");
26
- try {
27
- const data = await fetchInit();
28
- if (!isMounted) return;
29
- setTemplates(data.templates || []);
30
- setArticles(data.articles || []);
31
- setFaqs(data.faqs || []);
32
- setWebsiteName(data.website?.name || "Support");
33
- } catch (loadError) {
34
- if (!isMounted) return;
35
- setError(loadError instanceof Error ? loadError.message : "Unexpected error.");
36
- } finally {
37
- if (isMounted) setIsLoading(false);
38
- }
39
- };
40
- load();
41
- return () => { isMounted = false; };
42
- }, []);
43
-
44
- const filteredArticles = useMemo(() => {
45
- if (!searchQuery.trim()) return articles;
46
- const q = searchQuery.toLowerCase();
47
- return articles.filter(
48
- (a) => a.title.toLowerCase().includes(q) || stripHtml(a.content).toLowerCase().includes(q),
49
- );
50
- }, [articles, searchQuery]);
51
-
52
- const filteredFaqs = useMemo(() => {
53
- if (!searchQuery.trim()) return faqs;
54
- const q = searchQuery.toLowerCase();
55
- return faqs.filter(
56
- (f) => f.question.toLowerCase().includes(q) || f.answer.toLowerCase().includes(q),
57
- );
58
- }, [faqs, searchQuery]);
59
-
60
- const hasContent = articles.length > 0 || faqs.length > 0;
61
- const hasResults = filteredArticles.length > 0 || filteredFaqs.length > 0;
62
-
63
- return {
64
- templates, websiteName, isLoading, error, setError,
65
- searchQuery, setSearchQuery, dialogOpen, setDialogOpen,
66
- selectedArticle, setSelectedArticle,
67
- filteredArticles, filteredFaqs, hasContent, hasResults,
68
- };
69
- }
70
-
71
- /* ── Sub-components ──────────────────────────────── */
72
-
73
- function FaqItem({ question, answer }: { question: string; answer: string }) {
74
- const [open, setOpen] = useState(false);
75
-
76
- return (
77
- <div className="rounded-xl border border-slate-200 bg-white/50 transition-colors hover:bg-white/80">
78
- <button
79
- type="button"
80
- onClick={() => setOpen(!open)}
81
- className={cn(
82
- "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",
83
- open && "text-blue-600",
84
- )}
85
- aria-expanded={open}
86
- >
87
- {question}
88
- <span className={cn("shrink-0 text-slate-500 transition-transform duration-200", open && "rotate-180")}>
89
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"><path d="m6 9 6 6 6-6" /></svg>
90
- </span>
91
- </button>
92
- <div className={cn("grid grid-rows-[0fr] transition-[grid-template-rows] duration-250", open && "grid-rows-[1fr]")}>
93
- <div className="overflow-hidden">
94
- <div className="whitespace-pre-wrap px-4 pb-4 text-sm leading-relaxed text-slate-500">
95
- {answer}
96
- </div>
97
- </div>
98
- </div>
99
- </div>
100
- );
101
- }
102
-
103
- function Alert({ type, message, onDismiss }: { type: "error" | "success"; message: string; onDismiss?: () => void }) {
104
- return (
105
- <div
106
- className={cn(
107
- "flex items-start gap-3 rounded-xl border p-4 text-sm",
108
- type === "error" ? "border-red-200 bg-red-50 text-red-900" : "border-green-200 bg-green-50 text-green-900",
109
- )}
110
- role="alert"
111
- >
112
- <span className="mt-0.5 shrink-0">
113
- {type === "error" ? (
114
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="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>
115
- ) : (
116
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
117
- )}
118
- </span>
119
- <span className="flex-1">{message}</span>
120
- {onDismiss && (
121
- <button type="button" onClick={onDismiss} className="shrink-0 cursor-pointer border-none bg-transparent p-0 opacity-50 transition-opacity hover:opacity-100" aria-label="Dismiss">
122
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
123
- </button>
124
- )}
125
- </div>
126
- );
127
- }
128
-
129
- function HomeSkeleton() {
130
- return (
131
- <div>
132
- <div className="mx-auto flex max-w-5xl flex-col gap-6 px-6 pt-14 md:flex-row md:items-center md:justify-between">
133
- <div className="flex-1">
134
- <div className="mb-3 h-9 w-48 rounded-lg animate-pulse bg-slate-200" />
135
- <div className="h-5 w-64 rounded-md animate-pulse bg-slate-200" />
136
- </div>
137
- <div className="h-10 w-40 rounded-full animate-pulse bg-slate-200" />
138
- </div>
139
- <div className="mx-auto mt-8 max-w-5xl px-6">
140
- <div className="h-11 w-full rounded-xl animate-pulse bg-slate-200" />
141
- </div>
142
- <div className="mx-auto mt-8 grid max-w-5xl grid-cols-1 gap-8 px-6 md:grid-cols-2">
143
- <div className="space-y-3">
144
- <div className="h-5 w-36 rounded animate-pulse bg-slate-200" />
145
- {[1, 2, 3].map((i) => (
146
- <div key={i} className="h-20 rounded-xl animate-pulse bg-slate-200" />
147
- ))}
148
- </div>
149
- <div className="space-y-3">
150
- <div className="h-5 w-52 rounded animate-pulse bg-slate-200" />
151
- {[1, 2, 3].map((i) => (
152
- <div key={i} className="h-14 rounded-xl animate-pulse bg-slate-200" />
153
- ))}
154
- </div>
155
- </div>
156
- </div>
157
- );
158
- }
159
-
160
- /* ── Inline SVG icon paths (for readability) ──── */
161
- const svgArticle = <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="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>;
162
- const svgArticleSmall = <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="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>;
163
- const svgHelpCircle = <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="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>;
164
- const svgMessageCircle16 = <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" /></svg>;
165
- const svgMessageCircle22 = <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" /></svg>;
166
- const svgSearch = <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>;
167
- const svgArrowLeft = <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg>;
168
-
169
- /* ── Main component ──────────────────────────────── */
170
-
171
- export default function SupportPage() {
172
- const {
173
- templates, websiteName, isLoading, error, setError,
174
- searchQuery, setSearchQuery, dialogOpen, setDialogOpen,
175
- selectedArticle, setSelectedArticle,
176
- filteredArticles, filteredFaqs, hasContent, hasResults,
177
- } = useVicketSupport();
178
-
179
- if (isLoading) {
180
- return (
181
- <div className="min-h-screen bg-slate-50 font-[system-ui,-apple-system,sans-serif] antialiased">
182
- <HomeSkeleton />
183
- </div>
184
- );
185
- }
186
-
187
- if (selectedArticle) {
188
- return (
189
- <div className="min-h-screen bg-slate-50 font-[system-ui,-apple-system,sans-serif] antialiased">
190
- <div className="mx-auto max-w-5xl px-6">
191
- <div className="flex flex-col gap-4 pb-8 pt-14 md:flex-row md:items-center md:justify-between">
192
- <div>
193
- <h1 className="m-0 text-2xl font-bold tracking-tight text-slate-900 md:text-3xl">{websiteName}</h1>
194
- <p className="mt-1.5 text-slate-500">How can we help you today?</p>
195
- </div>
196
- {templates.length > 0 && (
197
- <button
198
- type="button"
199
- className="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"
200
- onClick={() => setDialogOpen(true)}
201
- >
202
- {svgMessageCircle16}
203
- Contact Support
204
- </button>
205
- )}
206
- </div>
207
-
208
- <div className="pb-16">
209
- <button
210
- type="button"
211
- onClick={() => setSelectedArticle(null)}
212
- className="-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"
213
- >
214
- {svgArrowLeft}
215
- Back to articles
216
- </button>
217
-
218
- <div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm md:p-8">
219
- <h2 className="m-0 text-xl font-bold tracking-tight text-slate-900 md:text-2xl">{selectedArticle.title}</h2>
220
- <div
221
- className="vk-message-content mt-5 text-sm leading-relaxed text-slate-500"
222
- dangerouslySetInnerHTML={{ __html: sanitizeHtml(selectedArticle.content) }}
223
- />
224
- </div>
225
- </div>
226
- </div>
227
-
228
- <style>{`
229
- .vk-message-content p { margin-bottom: 0.5rem; }
230
- .vk-message-content p:last-child { margin-bottom: 0; }
231
- .vk-message-content a { color: #2563eb; text-decoration: underline; }
232
- .vk-message-content ul, .vk-message-content ol { margin: 0.25rem 0; padding-left: 1.5rem; }
233
- .vk-message-content pre { overflow-x: auto; border-radius: 6px; background: #f8fafc; padding: 0.75rem; font-size: 0.75rem; }
234
- .vk-message-content code { font-family: monospace; font-size: 0.85em; }
235
- `}</style>
236
-
237
- <TicketDialog open={dialogOpen} onClose={() => setDialogOpen(false)} templates={templates} />
238
- </div>
239
- );
240
- }
241
-
242
- return (
243
- <div className="min-h-screen bg-slate-50 font-[system-ui,-apple-system,sans-serif] antialiased">
244
- <div className="mx-auto max-w-5xl px-6">
245
- <div className="flex flex-col gap-4 pb-8 pt-14 md:flex-row md:items-center md:justify-between">
246
- <div>
247
- <h1 className="m-0 text-2xl font-bold tracking-tight text-slate-900 md:text-3xl">{websiteName}</h1>
248
- <p className="mt-1.5 text-slate-500">How can we help you today?</p>
249
- </div>
250
- {templates.length > 0 && (
251
- <div className="flex items-center gap-3">
252
- <span className="hidden text-sm text-slate-500 md:block">Can&apos;t find what you&apos;re looking for?</span>
253
- <button
254
- type="button"
255
- className="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"
256
- onClick={() => setDialogOpen(true)}
257
- >
258
- {svgMessageCircle16}
259
- Contact Support
260
- </button>
261
- </div>
262
- )}
263
- </div>
264
-
265
- {error && (
266
- <div className="pb-4">
267
- <Alert type="error" message={error} onDismiss={() => setError("")} />
268
- </div>
269
- )}
270
-
271
- {hasContent && (
272
- <div className="pb-8">
273
- <div className="relative">
274
- <span className="pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-500">{svgSearch}</span>
275
- <input
276
- type="text"
277
- className="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"
278
- placeholder="Search articles and FAQs..."
279
- value={searchQuery}
280
- onChange={(e) => setSearchQuery(e.target.value)}
281
- />
282
- </div>
283
- </div>
284
- )}
285
-
286
- {hasResults && (
287
- <div className="grid grid-cols-1 gap-10 pb-16 md:grid-cols-2 md:gap-12">
288
- {filteredArticles.length > 0 && (
289
- <div>
290
- <div className="mb-4 flex items-center gap-2">
291
- <span className="text-blue-600">{svgArticle}</span>
292
- <h2 className="m-0 text-base font-bold tracking-tight text-slate-900">Popular Articles</h2>
293
- </div>
294
- <div className="space-y-1">
295
- {filteredArticles.map((article) => (
296
- <button
297
- key={article.id}
298
- type="button"
299
- className="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"
300
- onClick={() => setSelectedArticle(article)}
301
- >
302
- <div className="flex h-9 w-9 min-w-9 items-center justify-center rounded-lg bg-blue-50 text-blue-600">{svgArticleSmall}</div>
303
- <div className="min-w-0 flex-1">
304
- <h3 className="m-0 text-sm font-semibold leading-snug text-slate-900 transition-colors group-hover:text-blue-600">{article.title}</h3>
305
- {article.content && (
306
- <p className="mt-0.5 line-clamp-2 text-xs leading-normal text-slate-500">
307
- {stripHtml(article.content).substring(0, 150)}
308
- </p>
309
- )}
310
- </div>
311
- </button>
312
- ))}
313
- </div>
314
- </div>
315
- )}
316
-
317
- {filteredFaqs.length > 0 && (
318
- <div>
319
- <div className="mb-4 flex items-center gap-2">
320
- <span className="text-blue-600">{svgHelpCircle}</span>
321
- <h2 className="m-0 text-base font-bold tracking-tight text-slate-900">Frequently Asked Questions</h2>
322
- </div>
323
- <div className="space-y-2">
324
- {filteredFaqs.map((faq) => (
325
- <FaqItem key={faq.id} question={faq.question} answer={faq.answer} />
326
- ))}
327
- </div>
328
- </div>
329
- )}
330
- </div>
331
- )}
332
-
333
- {searchQuery.trim() && !hasResults && (
334
- <div className="pb-16 text-center">
335
- <p className="text-sm text-slate-500">No results found for &ldquo;{searchQuery}&rdquo;</p>
336
- </div>
337
- )}
338
-
339
- {!hasContent && !error && (
340
- <div className="pb-16 pt-4 text-center">
341
- <div className="mx-auto max-w-sm rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
342
- <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-blue-50 text-blue-600">{svgMessageCircle22}</div>
343
- <p className="m-0 text-sm font-medium text-slate-900">Need help? Our team is here for you.</p>
344
- <button
345
- type="button"
346
- className="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"
347
- onClick={() => setDialogOpen(true)}
348
- >
349
- Contact Support
350
- </button>
351
- </div>
352
- </div>
353
- )}
354
- </div>
355
-
356
- <TicketDialog open={dialogOpen} onClose={() => setDialogOpen(false)} templates={templates} />
357
- </div>
358
- );
359
- }