@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vicket/create-support",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "bin": {
5
5
  "create-support": "./bin/create-vicket-support.js"
6
6
  },
@@ -1,59 +1,6 @@
1
- import { NextRequest, NextResponse } from "next/server";
1
+ import { createVicketProxy } from "vicket/next";
2
2
 
3
- const VICKET_API_URL = (process.env.VICKET_API_URL || "").replace(/\/+$/, "");
4
- const VICKET_API_KEY = process.env.VICKET_API_KEY || "";
5
-
6
- async function proxy(
7
- req: NextRequest,
8
- { params }: { params: Promise<{ path: string[] }> },
9
- ) {
10
- if (!VICKET_API_URL || !VICKET_API_KEY) {
11
- return NextResponse.json(
12
- { error: "Missing VICKET_API_URL or VICKET_API_KEY server environment variables." },
13
- { status: 500 },
14
- );
15
- }
16
-
17
- const { path } = await params;
18
- const subpath = path.join("/");
19
- const url = new URL(`${VICKET_API_URL}/public/support/${subpath}`);
20
-
21
- // Forward query params
22
- req.nextUrl.searchParams.forEach((value, key) => {
23
- url.searchParams.set(key, value);
24
- });
25
-
26
- const headers: HeadersInit = { "X-Api-Key": VICKET_API_KEY };
27
- const contentType = req.headers.get("content-type") || "";
28
-
29
- let body: BodyInit | null = null;
30
-
31
- if (req.method !== "GET" && req.method !== "HEAD") {
32
- if (contentType.includes("multipart/form-data")) {
33
- // Stream FormData as-is; let fetch set the boundary
34
- body = await req.blob();
35
- headers["Content-Type"] = contentType;
36
- } else {
37
- body = await req.text();
38
- headers["Content-Type"] = contentType || "application/json";
39
- }
40
- }
41
-
42
- const upstream = await fetch(url.toString(), {
43
- method: req.method,
44
- headers,
45
- body,
46
- });
47
-
48
- const responseBody = await upstream.arrayBuffer();
49
-
50
- return new NextResponse(responseBody, {
51
- status: upstream.status,
52
- headers: {
53
- "Content-Type": upstream.headers.get("Content-Type") || "application/json",
54
- },
55
- });
56
- }
3
+ const proxy = createVicketProxy();
57
4
 
58
5
  export const GET = proxy;
59
6
  export const POST = proxy;
@@ -0,0 +1,154 @@
1
+ "use client";
2
+
3
+ import { type FormEvent, useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { cn, sendReply } from "vicket";
6
+
7
+ /* ---------------------------------------------- */
8
+ /* Alert component */
9
+ /* ---------------------------------------------- */
10
+ function Alert({
11
+ type,
12
+ message,
13
+ onDismiss,
14
+ }: {
15
+ type: "error" | "success";
16
+ message: string;
17
+ onDismiss?: () => void;
18
+ }) {
19
+ return (
20
+ <div
21
+ className={cn("vk-alert vk-slide-up", type === "error" ? "error" : "success")}
22
+ role="alert"
23
+ >
24
+ <span>{type === "error" ? "\u26A0" : "\u2713"}</span>
25
+ <span>{message}</span>
26
+ {onDismiss && (
27
+ <button
28
+ type="button"
29
+ onClick={onDismiss}
30
+ className="vk-alert-dismiss"
31
+ aria-label="Dismiss"
32
+ >
33
+ &#10005;
34
+ </button>
35
+ )}
36
+ </div>
37
+ );
38
+ }
39
+
40
+ /* ---------------------------------------------- */
41
+ /* Reply Form */
42
+ /* ---------------------------------------------- */
43
+ export default function ReplyForm({ token }: { token: string }) {
44
+ const router = useRouter();
45
+ const [content, setContent] = useState("");
46
+ const [files, setFiles] = useState<File[]>([]);
47
+ const [isSending, setIsSending] = useState(false);
48
+ const [error, setError] = useState("");
49
+ const [success, setSuccess] = useState("");
50
+
51
+ const removeFile = (index: number) => {
52
+ setFiles((prev) => prev.filter((_, i) => i !== index));
53
+ };
54
+
55
+ const onSubmitReply = async (event: FormEvent<HTMLFormElement>) => {
56
+ event.preventDefault();
57
+ setError("");
58
+ setSuccess("");
59
+
60
+ if (!content.trim() && files.length === 0) {
61
+ setError("Reply content is required.");
62
+ return;
63
+ }
64
+
65
+ if (!token.trim()) {
66
+ setError("Missing ticket token.");
67
+ return;
68
+ }
69
+
70
+ setIsSending(true);
71
+ try {
72
+ await sendReply(token, content.trim(), files);
73
+ setContent("");
74
+ setFiles([]);
75
+ setSuccess("Reply sent.");
76
+ router.refresh();
77
+ } catch (replyError) {
78
+ setError(replyError instanceof Error ? replyError.message : "Unexpected error.");
79
+ } finally {
80
+ setIsSending(false);
81
+ }
82
+ };
83
+
84
+ return (
85
+ <>
86
+ {/* Alerts */}
87
+ {error && (
88
+ <Alert type="error" message={error} onDismiss={() => setError("")} />
89
+ )}
90
+ {success && (
91
+ <Alert type="success" message={success} onDismiss={() => setSuccess("")} />
92
+ )}
93
+
94
+ {/* Compose area */}
95
+ <div className="vk-compose">
96
+ <form className="vk-stack" onSubmit={onSubmitReply}>
97
+ <textarea
98
+ className="vk-textarea"
99
+ value={content}
100
+ onChange={(e) => setContent(e.target.value)}
101
+ placeholder="Write your reply..."
102
+ />
103
+
104
+ <div className="vk-compose-row">
105
+ {/* File input */}
106
+ <div className="vk-compose-files">
107
+ <label className="vk-browse-btn">
108
+ &#128206; Browse files
109
+ <input
110
+ type="file"
111
+ multiple
112
+ onChange={(e) => {
113
+ const newFiles = Array.from(e.target.files || []);
114
+ setFiles((prev) => [...prev, ...newFiles]);
115
+ e.target.value = "";
116
+ }}
117
+ />
118
+ </label>
119
+ {files.map((file, i) => (
120
+ <span className="vk-file-chip" key={`${file.name}-${i}`}>
121
+ &#128206;
122
+ <span className="vk-file-chip-name">{file.name}</span>
123
+ <button
124
+ type="button"
125
+ onClick={() => removeFile(i)}
126
+ aria-label={`Remove ${file.name}`}
127
+ >
128
+ &#10005;
129
+ </button>
130
+ </span>
131
+ ))}
132
+ </div>
133
+
134
+ {/* Send */}
135
+ <button
136
+ className="vk-button primary"
137
+ disabled={isSending}
138
+ type="submit"
139
+ >
140
+ {isSending ? (
141
+ <>
142
+ <span className="vk-spinner" />
143
+ Sending...
144
+ </>
145
+ ) : (
146
+ "Send"
147
+ )}
148
+ </button>
149
+ </div>
150
+ </form>
151
+ </div>
152
+ </>
153
+ );
154
+ }
@@ -0,0 +1,298 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import "../vicket.css";
5
+ import type { Article, Faq, Template } from "vicket";
6
+ import { cn, stripHtml, sanitizeHtml } from "vicket";
7
+ import TicketDialog from "./TicketDialog";
8
+
9
+ /* ---------------------------------------------- */
10
+ /* Alert component */
11
+ /* ---------------------------------------------- */
12
+ function Alert({
13
+ type,
14
+ message,
15
+ onDismiss,
16
+ }: {
17
+ type: "error" | "success";
18
+ message: string;
19
+ onDismiss?: () => void;
20
+ }) {
21
+ return (
22
+ <div
23
+ className={cn("vk-alert vk-slide-up", type === "error" ? "error" : "success")}
24
+ role="alert"
25
+ >
26
+ <span>{type === "error" ? "\u26A0" : "\u2713"}</span>
27
+ <span>{message}</span>
28
+ {onDismiss && (
29
+ <button
30
+ type="button"
31
+ onClick={onDismiss}
32
+ className="vk-alert-dismiss"
33
+ aria-label="Dismiss"
34
+ >
35
+ &#10005;
36
+ </button>
37
+ )}
38
+ </div>
39
+ );
40
+ }
41
+
42
+ /* ---------------------------------------------- */
43
+ /* FAQ Accordion item */
44
+ /* ---------------------------------------------- */
45
+ function FaqItem({ question, answer }: { question: string; answer: string }) {
46
+ const [open, setOpen] = useState(false);
47
+
48
+ return (
49
+ <div className="vk-faq-item">
50
+ <button
51
+ type="button"
52
+ onClick={() => setOpen(!open)}
53
+ className="vk-faq-question"
54
+ aria-expanded={open}
55
+ >
56
+ {question}
57
+ <span className={cn("vk-faq-chevron", open && "open")}>&#9660;</span>
58
+ </button>
59
+ <div className={cn("vk-faq-body", open && "open")}>
60
+ <div>
61
+ <div className="vk-faq-answer">{answer}</div>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ /* ---------------------------------------------- */
69
+ /* Props */
70
+ /* ---------------------------------------------- */
71
+ type SupportContentProps = {
72
+ templates: Template[];
73
+ articles: Article[];
74
+ faqs: Faq[];
75
+ websiteName: string;
76
+ initialError: string;
77
+ };
78
+
79
+ /* ---------------------------------------------- */
80
+ /* Main component */
81
+ /* ---------------------------------------------- */
82
+ export default function SupportContent({
83
+ templates,
84
+ articles,
85
+ faqs,
86
+ websiteName,
87
+ initialError,
88
+ }: SupportContentProps) {
89
+ const [error, setError] = useState(initialError);
90
+ const [searchQuery, setSearchQuery] = useState("");
91
+ const [dialogOpen, setDialogOpen] = useState(false);
92
+ const [selectedArticle, setSelectedArticle] = useState<Article | null>(null);
93
+
94
+ /* Filtered articles & FAQs by search query */
95
+ const filteredArticles = useMemo(() => {
96
+ if (!searchQuery.trim()) return articles;
97
+ const q = searchQuery.toLowerCase();
98
+ return articles.filter(
99
+ (a) =>
100
+ a.title.toLowerCase().includes(q) ||
101
+ stripHtml(a.content).toLowerCase().includes(q),
102
+ );
103
+ }, [articles, searchQuery]);
104
+
105
+ const filteredFaqs = useMemo(() => {
106
+ if (!searchQuery.trim()) return faqs;
107
+ const q = searchQuery.toLowerCase();
108
+ return faqs.filter(
109
+ (f) =>
110
+ f.question.toLowerCase().includes(q) ||
111
+ f.answer.toLowerCase().includes(q),
112
+ );
113
+ }, [faqs, searchQuery]);
114
+
115
+ const hasContent = articles.length > 0 || faqs.length > 0;
116
+ const hasResults = filteredArticles.length > 0 || filteredFaqs.length > 0;
117
+
118
+ /* Article viewer */
119
+ if (selectedArticle) {
120
+ return (
121
+ <div className="vk-shell">
122
+ <div className="vk-page vk-animate-in">
123
+ {/* Hero stays visible */}
124
+ <div className="vk-hero-row">
125
+ <div>
126
+ <h1 className="vk-hero-title">{websiteName}</h1>
127
+ <p className="vk-hero-subtitle">How can we help you today?</p>
128
+ </div>
129
+ {templates.length > 0 && (
130
+ <button
131
+ type="button"
132
+ className="vk-button primary pill"
133
+ onClick={() => setDialogOpen(true)}
134
+ >
135
+ &#128172; Contact Support
136
+ </button>
137
+ )}
138
+ </div>
139
+
140
+ {/* Article content */}
141
+ <div className="vk-article-viewer">
142
+ <button
143
+ type="button"
144
+ onClick={() => setSelectedArticle(null)}
145
+ className="vk-back-button"
146
+ >
147
+ &#8592; Back to articles
148
+ </button>
149
+
150
+ <div className="vk-article-viewer-card">
151
+ <h2 className="vk-article-viewer-title">{selectedArticle.title}</h2>
152
+ <div
153
+ className="vk-article-viewer-content vk-message-html"
154
+ dangerouslySetInnerHTML={{
155
+ __html: sanitizeHtml(selectedArticle.content),
156
+ }}
157
+ />
158
+ </div>
159
+ </div>
160
+ </div>
161
+
162
+ <TicketDialog
163
+ open={dialogOpen}
164
+ onClose={() => setDialogOpen(false)}
165
+ templates={templates}
166
+ />
167
+ </div>
168
+ );
169
+ }
170
+
171
+ /* Home view */
172
+ return (
173
+ <div className="vk-shell">
174
+ <div className="vk-page vk-animate-in">
175
+ {/* Row 1 - Hero */}
176
+ <div className="vk-hero-row">
177
+ <div>
178
+ <h1 className="vk-hero-title">{websiteName}</h1>
179
+ <p className="vk-hero-subtitle">How can we help you today?</p>
180
+ </div>
181
+ {templates.length > 0 && (
182
+ <div className="vk-hero-cta">
183
+ <span className="vk-hero-cta-hint">
184
+ Can&apos;t find what you&apos;re looking for?
185
+ </span>
186
+ <button
187
+ type="button"
188
+ className="vk-button primary pill"
189
+ onClick={() => setDialogOpen(true)}
190
+ >
191
+ &#128172; Contact Support
192
+ </button>
193
+ </div>
194
+ )}
195
+ </div>
196
+
197
+ {/* Alerts */}
198
+ {error && (
199
+ <Alert type="error" message={error} onDismiss={() => setError("")} />
200
+ )}
201
+
202
+ {/* Row 2 - Search bar */}
203
+ {hasContent && (
204
+ <div className="vk-search-wrap">
205
+ <span className="vk-search-icon">&#128269;</span>
206
+ <input
207
+ type="text"
208
+ className="vk-search-input"
209
+ placeholder="Search articles and FAQs..."
210
+ value={searchQuery}
211
+ onChange={(e) => setSearchQuery(e.target.value)}
212
+ />
213
+ </div>
214
+ )}
215
+
216
+ {/* Row 3 - Split content */}
217
+ {hasResults && (
218
+ <div className="vk-content-grid">
219
+ {/* Left: Articles */}
220
+ {filteredArticles.length > 0 && (
221
+ <div>
222
+ <div className="vk-section-title-row">
223
+ <span className="vk-section-title-icon">&#128196;</span>
224
+ <h2 className="vk-section-title">Popular Articles</h2>
225
+ </div>
226
+ <div className="vk-article-list">
227
+ {filteredArticles.map((article) => (
228
+ <button
229
+ key={article.id}
230
+ type="button"
231
+ className="vk-article-card"
232
+ onClick={() => setSelectedArticle(article)}
233
+ >
234
+ <div className="vk-article-icon" aria-hidden="true">
235
+ &#128196;
236
+ </div>
237
+ <div>
238
+ <h3 className="vk-article-title">{article.title}</h3>
239
+ {article.content && (
240
+ <p className="vk-article-preview">
241
+ {stripHtml(article.content).substring(0, 150)}
242
+ </p>
243
+ )}
244
+ </div>
245
+ </button>
246
+ ))}
247
+ </div>
248
+ </div>
249
+ )}
250
+
251
+ {/* Right: FAQs */}
252
+ {filteredFaqs.length > 0 && (
253
+ <div>
254
+ <div className="vk-section-title-row">
255
+ <span className="vk-section-title-icon">&#10068;</span>
256
+ <h2 className="vk-section-title">Frequently Asked Questions</h2>
257
+ </div>
258
+ <div className="vk-faq-list">
259
+ {filteredFaqs.map((faq) => (
260
+ <FaqItem key={faq.id} question={faq.question} answer={faq.answer} />
261
+ ))}
262
+ </div>
263
+ </div>
264
+ )}
265
+ </div>
266
+ )}
267
+
268
+ {/* No search results */}
269
+ {searchQuery.trim() && !hasResults && (
270
+ <div className="vk-no-results">
271
+ No results found for &ldquo;{searchQuery}&rdquo;
272
+ </div>
273
+ )}
274
+
275
+ {/* Empty state - no articles and no FAQs at all */}
276
+ {!hasContent && !error && (
277
+ <div className="vk-cta-card">
278
+ <div className="vk-cta-icon">&#128172;</div>
279
+ <p className="vk-cta-text">Need help? Our team is here for you.</p>
280
+ <button
281
+ type="button"
282
+ className="vk-button primary pill"
283
+ onClick={() => setDialogOpen(true)}
284
+ >
285
+ Contact Support
286
+ </button>
287
+ </div>
288
+ )}
289
+ </div>
290
+
291
+ <TicketDialog
292
+ open={dialogOpen}
293
+ onClose={() => setDialogOpen(false)}
294
+ templates={templates}
295
+ />
296
+ </div>
297
+ );
298
+ }
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
 
3
3
  import { type FormEvent, useMemo, useRef, useState } from "react";
4
- import type { FormValues, Template } from "../../utils/vicket/types";
5
- import { cn } from "../../utils/vicket/utils";
6
- import { createTicket, initialFormValues } from "../../utils/vicket/api";
4
+ import type { FormValues, Template } from "vicket";
5
+ import { cn } from "vicket";
6
+ import { createTicket, initialFormValues } from "vicket";
7
7
 
8
8
  const MAX_FILES = 3;
9
9
  const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB