@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.
Files changed (55) hide show
  1. package/README.md +52 -0
  2. package/bin/create-vicket-support.js +389 -0
  3. package/package.json +18 -0
  4. package/templates/next/src/app/api/vicket/[...path]/route.ts +59 -0
  5. package/templates/next/src/app/components/vicket/TicketDialog.tsx +514 -0
  6. package/templates/next/src/app/support/page.tsx +358 -0
  7. package/templates/next/src/app/ticket/page.tsx +483 -0
  8. package/templates/next/src/app/utils/vicket/api.ts +149 -0
  9. package/templates/next/src/app/utils/vicket/types.ts +85 -0
  10. package/templates/next/src/app/utils/vicket/utils.ts +49 -0
  11. package/templates/next/src/app/vicket.css +1325 -0
  12. package/templates/nuxt/app/assets/css/vicket.css +1325 -0
  13. package/templates/nuxt/app/components/VicketTicketDialog.vue +499 -0
  14. package/templates/nuxt/app/composables/useVicket.ts +274 -0
  15. package/templates/nuxt/app/pages/support.vue +303 -0
  16. package/templates/nuxt/app/pages/ticket.vue +434 -0
  17. package/templates/nuxt/server/api/vicket/[...path].ts +85 -0
  18. package/templates/sveltekit/src/lib/vicket/TicketDialog.svelte +459 -0
  19. package/templates/sveltekit/src/lib/vicket/api.ts +162 -0
  20. package/templates/sveltekit/src/lib/vicket/types.ts +87 -0
  21. package/templates/sveltekit/src/lib/vicket/utils.ts +55 -0
  22. package/templates/sveltekit/src/lib/vicket.css +1325 -0
  23. package/templates/sveltekit/src/routes/api/vicket/[...path]/+server.ts +77 -0
  24. package/templates/sveltekit/src/routes/support/+page.svelte +316 -0
  25. package/templates/sveltekit/src/routes/ticket/+page.svelte +418 -0
  26. package/templates-tailwind/next/src/app/api/vicket/init/route.ts +24 -0
  27. package/templates-tailwind/next/src/app/api/vicket/messages/route.ts +36 -0
  28. package/templates-tailwind/next/src/app/api/vicket/thread/route.ts +27 -0
  29. package/templates-tailwind/next/src/app/api/vicket/tickets/route.ts +37 -0
  30. package/templates-tailwind/next/src/app/support/page.tsx +5 -0
  31. package/templates-tailwind/next/src/app/ticket/page.tsx +10 -0
  32. package/templates-tailwind/next/src/components/vicket/support-page.tsx +359 -0
  33. package/templates-tailwind/next/src/components/vicket/ticket-dialog.tsx +306 -0
  34. package/templates-tailwind/next/src/components/vicket/ticket-page.tsx +425 -0
  35. package/templates-tailwind/next/src/lib/vicket.ts +257 -0
  36. package/templates-tailwind/nuxt/app/components/VicketSupportPage.vue +317 -0
  37. package/templates-tailwind/nuxt/app/components/VicketTicketDialog.vue +444 -0
  38. package/templates-tailwind/nuxt/app/components/VicketTicketPage.vue +449 -0
  39. package/templates-tailwind/nuxt/app/composables/use-vicket.ts +249 -0
  40. package/templates-tailwind/nuxt/app/pages/support.vue +3 -0
  41. package/templates-tailwind/nuxt/app/pages/ticket.vue +3 -0
  42. package/templates-tailwind/nuxt/server/api/vicket/init.get.ts +22 -0
  43. package/templates-tailwind/nuxt/server/api/vicket/messages.post.ts +56 -0
  44. package/templates-tailwind/nuxt/server/api/vicket/thread.get.ts +26 -0
  45. package/templates-tailwind/nuxt/server/api/vicket/tickets.post.ts +53 -0
  46. package/templates-tailwind/sveltekit/src/lib/vicket/SupportPage.svelte +395 -0
  47. package/templates-tailwind/sveltekit/src/lib/vicket/TicketDialog.svelte +406 -0
  48. package/templates-tailwind/sveltekit/src/lib/vicket/TicketPage.svelte +465 -0
  49. package/templates-tailwind/sveltekit/src/lib/vicket/index.ts +257 -0
  50. package/templates-tailwind/sveltekit/src/routes/api/vicket/init/+server.ts +22 -0
  51. package/templates-tailwind/sveltekit/src/routes/api/vicket/messages/+server.ts +40 -0
  52. package/templates-tailwind/sveltekit/src/routes/api/vicket/thread/+server.ts +25 -0
  53. package/templates-tailwind/sveltekit/src/routes/api/vicket/tickets/+server.ts +37 -0
  54. package/templates-tailwind/sveltekit/src/routes/support/+page.svelte +5 -0
  55. package/templates-tailwind/sveltekit/src/routes/ticket/+page.svelte +5 -0
@@ -0,0 +1,306 @@
1
+ "use client";
2
+
3
+ import { type FormEvent, useMemo, useState } from "react";
4
+ import type { Template, FormValues } from "@/lib/vicket";
5
+ import { createTicket, initialFormValues, cn } from "@/lib/vicket";
6
+
7
+ /* ── Custom hook ─────────────────────────────────── */
8
+
9
+ function useTicketDialog(templates: Template[], onClose: () => void) {
10
+ const [step, setStep] = useState<"identify" | "details" | "success">("identify");
11
+ const [selectedTemplateId, setSelectedTemplateId] = useState(templates.length > 0 ? templates[0].id : "");
12
+ const [form, setForm] = useState<FormValues>(initialFormValues);
13
+ const [isSubmitting, setIsSubmitting] = useState(false);
14
+ const [error, setError] = useState("");
15
+ const [emailLimitReached, setEmailLimitReached] = useState(false);
16
+
17
+ const selectedTemplate = useMemo(
18
+ () => templates.find((t) => t.id === selectedTemplateId) || null,
19
+ [templates, selectedTemplateId],
20
+ );
21
+
22
+ const orderedQuestions = useMemo(
23
+ () => [...(selectedTemplate?.questions || [])].sort((a, b) => a.order - b.order),
24
+ [selectedTemplate],
25
+ );
26
+
27
+ const canContinue = form.email.trim().length > 0;
28
+
29
+ const resetAndClose = () => {
30
+ setStep("identify");
31
+ setForm(initialFormValues);
32
+ setError("");
33
+ setSelectedTemplateId(templates.length > 0 ? templates[0].id : "");
34
+ onClose();
35
+ };
36
+
37
+ const updateAnswer = (questionId: string, value: unknown) => {
38
+ setForm((prev) => ({ ...prev, answers: { ...prev.answers, [questionId]: value } }));
39
+ };
40
+
41
+ const toggleCheckboxValue = (questionId: string, value: string, checked: boolean) => {
42
+ const current = Array.isArray(form.answers[questionId]) ? (form.answers[questionId] as string[]) : [];
43
+ const next = checked ? [...new Set([...current, value])] : current.filter((item) => item !== value);
44
+ updateAnswer(questionId, next);
45
+ };
46
+
47
+ const validateRequired = () => {
48
+ if (!selectedTemplate) return "Please select a template.";
49
+ if (!form.email.trim()) return "Email is required.";
50
+ if (!form.title.trim()) return "Subject is required.";
51
+ for (const question of orderedQuestions) {
52
+ if (!question.required) continue;
53
+ const value = form.answers[question.id];
54
+ if (question.type === "CHECKBOX") {
55
+ if (!Array.isArray(value) || value.length === 0) return `"${question.label}" is required.`;
56
+ continue;
57
+ }
58
+ if (question.type === "FILE") {
59
+ if (!(value instanceof File)) return `"${question.label}" is required.`;
60
+ continue;
61
+ }
62
+ if (value === null || value === undefined || String(value).trim() === "") return `"${question.label}" is required.`;
63
+ }
64
+ return "";
65
+ };
66
+
67
+ const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
68
+ event.preventDefault();
69
+ setError("");
70
+ const validationError = validateRequired();
71
+ if (validationError) { setError(validationError); return; }
72
+ if (!selectedTemplate) { setError("Template is required."); return; }
73
+
74
+ setIsSubmitting(true);
75
+ try {
76
+ const fileQuestionIds = orderedQuestions
77
+ .filter((q) => q.type === "FILE" && form.answers[q.id] instanceof File)
78
+ .map((q) => q.id);
79
+
80
+ const result = await createTicket({
81
+ email: form.email.trim(),
82
+ title: form.title.trim(),
83
+ templateId: selectedTemplate.id,
84
+ answers: { ...form.answers },
85
+ hasFiles: fileQuestionIds.length > 0,
86
+ fileQuestionIds,
87
+ });
88
+
89
+ setEmailLimitReached(result.emailLimitReached ?? false);
90
+ setStep("success");
91
+ } catch (submitError) {
92
+ setError(submitError instanceof Error ? submitError.message : "Unexpected error.");
93
+ } finally {
94
+ setIsSubmitting(false);
95
+ }
96
+ };
97
+
98
+ return {
99
+ step, setStep, selectedTemplateId, setSelectedTemplateId,
100
+ form, setForm, isSubmitting, error, setError, emailLimitReached,
101
+ selectedTemplate, orderedQuestions, canContinue,
102
+ resetAndClose, updateAnswer, toggleCheckboxValue, handleSubmit,
103
+ };
104
+ }
105
+
106
+ /* ── Sub-components ──────────────────────────────── */
107
+
108
+ function Alert({ type, message, onDismiss }: { type: "error" | "success"; message: string; onDismiss?: () => void }) {
109
+ return (
110
+ <div
111
+ className={cn(
112
+ "flex items-start gap-3 rounded-xl border p-4 text-sm",
113
+ type === "error" ? "border-red-200 bg-red-50 text-red-900" : "border-green-200 bg-green-50 text-green-900",
114
+ )}
115
+ role="alert"
116
+ >
117
+ <span className="mt-0.5 shrink-0">
118
+ {type === "error" ? (
119
+ <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>
120
+ ) : (
121
+ <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>
122
+ )}
123
+ </span>
124
+ <span className="flex-1">{message}</span>
125
+ {onDismiss && (
126
+ <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">
127
+ <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>
128
+ </button>
129
+ )}
130
+ </div>
131
+ );
132
+ }
133
+
134
+ /* ── Main component ──────────────────────────────── */
135
+
136
+ export default function TicketDialog({ open, onClose, templates }: { open: boolean; onClose: () => void; templates: Template[] }) {
137
+ const {
138
+ step, setStep, selectedTemplateId, setSelectedTemplateId,
139
+ form, setForm, isSubmitting, error, setError, emailLimitReached,
140
+ selectedTemplate, orderedQuestions, canContinue,
141
+ resetAndClose, updateAnswer, toggleCheckboxValue, handleSubmit,
142
+ } = useTicketDialog(templates, onClose);
143
+
144
+ if (!open) return null;
145
+
146
+ return (
147
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm" onClick={resetAndClose}>
148
+ <div className="relative mx-4 w-full max-w-lg overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-xl" onClick={(e) => e.stopPropagation()}>
149
+ <button type="button" onClick={resetAndClose} className="absolute right-4 top-4 z-10 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border-none bg-transparent text-slate-500 transition-colors hover:bg-slate-50 hover:text-slate-900" aria-label="Close">
150
+ <svg width="18" height="18" 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>
151
+ </button>
152
+
153
+ {step === "success" && (
154
+ <div className="px-6 py-12 text-center">
155
+ <div className="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-full bg-green-50 text-green-600">
156
+ <svg width="28" height="28" 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>
157
+ </div>
158
+ <h2 className="m-0 text-xl font-bold text-slate-900">Ticket submitted!</h2>
159
+ <p className="mt-2 text-sm text-slate-500">
160
+ {emailLimitReached
161
+ ? "Your ticket was created, but the daily email limit for this service has been reached. No confirmation email was sent. Please consider using a self-hosted email delivery setup for unlimited emails."
162
+ : "Check your email for a secure link to follow your ticket."}
163
+ </p>
164
+ <button type="button" className="mt-6 inline-flex items-center gap-2 !rounded-full border border-slate-200 bg-white !px-7 !py-3 text-sm font-semibold text-slate-900 cursor-pointer transition-all hover:border-slate-300 hover:bg-slate-50" onClick={resetAndClose}>Close</button>
165
+ </div>
166
+ )}
167
+
168
+ {step === "identify" && (
169
+ <div className="px-6 py-6">
170
+ <h2 className="m-0 text-lg font-bold text-slate-900">Submit a request</h2>
171
+ <p className="mt-1 text-sm text-slate-500">We&apos;ll get back to you as soon as possible.</p>
172
+
173
+ <div className="mt-5 space-y-5">
174
+ {error && <Alert type="error" message={error} onDismiss={() => setError("")} />}
175
+
176
+ <div className="space-y-1.5">
177
+ <label className="text-sm font-semibold text-slate-900" htmlFor="vk-dialog-email">Email<span className="text-red-600"> *</span></label>
178
+ <div className="relative">
179
+ <span className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-slate-500">
180
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"><rect width="20" height="16" x="2" y="4" rx="2" /><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" /></svg>
181
+ </span>
182
+ <input id="vk-dialog-email" className="w-full rounded-lg border border-slate-200 bg-slate-50 py-2.5 pl-10 pr-3 text-sm text-slate-900 transition-all duration-150 placeholder:text-slate-500/60 focus:border-blue-600 focus:outline-none focus:ring-3 focus:ring-blue-600/10" type="email" placeholder="you@example.com" value={form.email} onChange={(e) => setForm((prev) => ({ ...prev, email: e.target.value }))} required />
183
+ </div>
184
+ <p className="text-xs text-slate-500">We&apos;ll contact you at this address.</p>
185
+ </div>
186
+
187
+ {templates.length > 1 && (
188
+ <div className="space-y-2">
189
+ <p className="m-0 text-sm font-semibold text-slate-900">What can we help you with?</p>
190
+ <div className="grid gap-2">
191
+ {templates.map((template) => (
192
+ <button
193
+ key={template.id}
194
+ type="button"
195
+ className={cn(
196
+ "flex w-full cursor-pointer items-start gap-3 rounded-xl border border-slate-200 bg-slate-50 p-3.5 text-left transition-all duration-150 hover:border-slate-300",
197
+ selectedTemplateId === template.id && "!border-blue-600 !bg-blue-50 ring-3 ring-blue-600/12",
198
+ )}
199
+ onClick={() => setSelectedTemplateId(template.id)}
200
+ >
201
+ <span className={cn("mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border-2 transition-colors", selectedTemplateId === template.id ? "border-blue-600 bg-blue-600" : "border-slate-300 bg-transparent")}>
202
+ {selectedTemplateId === template.id && <span className="block h-1.5 w-1.5 rounded-full bg-white" />}
203
+ </span>
204
+ <div>
205
+ <span className="block text-sm font-semibold text-slate-900">{template.name}</span>
206
+ {template.description && <span className="mt-0.5 block text-xs text-slate-500">{template.description}</span>}
207
+ </div>
208
+ </button>
209
+ ))}
210
+ </div>
211
+ </div>
212
+ )}
213
+
214
+ <button type="button" className="inline-flex w-full items-center justify-center gap-2 rounded-lg border-none bg-blue-600 px-5 py-2.5 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 disabled:opacity-60 disabled:cursor-not-allowed" disabled={!canContinue} onClick={() => { setError(""); setStep("details"); }}>
215
+ Continue
216
+ </button>
217
+ </div>
218
+ </div>
219
+ )}
220
+
221
+ {step === "details" && (
222
+ <div className="px-6 py-6">
223
+ <button type="button" className="-ml-2 -mt-1 mb-3 inline-flex cursor-pointer items-center gap-1 rounded-lg border-none bg-transparent px-2 py-1 text-sm font-medium text-slate-500 transition-colors hover:bg-slate-50 hover:text-slate-900" onClick={() => { setError(""); setStep("identify"); }}>
224
+ <svg width="14" height="14" 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>
225
+ Back
226
+ </button>
227
+
228
+ <h2 className="m-0 text-lg font-bold text-slate-900">{selectedTemplate?.name || "Ticket details"}</h2>
229
+
230
+ <form className="mt-5 space-y-4" onSubmit={handleSubmit}>
231
+ {error && <Alert type="error" message={error} onDismiss={() => setError("")} />}
232
+
233
+ <div className="space-y-1.5">
234
+ <label className="text-sm font-semibold text-slate-900" htmlFor="vk-dialog-subject">Subject<span className="text-red-600"> *</span></label>
235
+ <input id="vk-dialog-subject" className="w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm text-slate-900 transition-all duration-150 placeholder:text-slate-500/60 focus:border-blue-600 focus:outline-none focus:ring-3 focus:ring-blue-600/10" type="text" placeholder="Brief description of your issue" value={form.title} onChange={(e) => setForm((prev) => ({ ...prev, title: e.target.value }))} required />
236
+ </div>
237
+
238
+ {orderedQuestions.map((question) => (
239
+ <div className="space-y-1.5" key={question.id}>
240
+ <label className="text-sm font-semibold text-slate-900">{question.label}{question.required && <span className="text-red-600"> *</span>}</label>
241
+
242
+ {question.type === "TEXT" && (
243
+ <input className="w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm text-slate-900 transition-all duration-150 focus:border-blue-600 focus:outline-none focus:ring-3 focus:ring-blue-600/10" type="text" value={String(form.answers[question.id] || "")} onChange={(e) => updateAnswer(question.id, e.target.value)} />
244
+ )}
245
+ {question.type === "TEXTAREA" && (
246
+ <textarea className="min-h-[100px] w-full resize-y rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm text-slate-900 transition-all duration-150 focus:border-blue-600 focus:outline-none focus:ring-3 focus:ring-blue-600/10" value={String(form.answers[question.id] || "")} onChange={(e) => updateAnswer(question.id, e.target.value)} />
247
+ )}
248
+ {question.type === "DATE" && (
249
+ <input className="w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm text-slate-900 transition-all duration-150 focus:border-blue-600 focus:outline-none focus:ring-3 focus:ring-blue-600/10" type="date" value={String(form.answers[question.id] || "")} onChange={(e) => updateAnswer(question.id, e.target.value)} />
250
+ )}
251
+ {question.type === "SELECT" && (
252
+ <select className="w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm text-slate-900 transition-all duration-150 focus:border-blue-600 focus:outline-none focus:ring-3 focus:ring-blue-600/10" value={String(form.answers[question.id] || "")} onChange={(e) => updateAnswer(question.id, e.target.value)}>
253
+ <option value="">Select an option</option>
254
+ {(question.options || []).map((option) => <option key={option.id} value={option.value}>{option.label}</option>)}
255
+ </select>
256
+ )}
257
+ {question.type === "CHECKBOX" && (
258
+ <div className="space-y-2 pt-1">
259
+ {(question.options || []).map((option) => (
260
+ <label key={option.id} className="flex items-center gap-2.5 text-sm text-slate-900">
261
+ <input type="checkbox" className="h-4 w-4 rounded accent-blue-600" checked={Array.isArray(form.answers[question.id]) && (form.answers[question.id] as string[]).includes(option.value)} onChange={(e) => toggleCheckboxValue(question.id, option.value, e.target.checked)} />
262
+ <span>{option.label}</span>
263
+ </label>
264
+ ))}
265
+ </div>
266
+ )}
267
+ {question.type === "FILE" && (
268
+ <>
269
+ {form.answers[question.id] instanceof File ? (
270
+ <div className="flex flex-wrap gap-1.5">
271
+ <span className="inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-slate-50 px-3 py-1.5 text-xs text-slate-500">
272
+ {(form.answers[question.id] as File).name}
273
+ <button type="button" onClick={() => updateAnswer(question.id, null)} className="flex cursor-pointer items-center border-none bg-transparent p-0 text-slate-500 transition-colors hover:text-red-600" aria-label="Remove file">
274
+ <svg width="14" height="14" 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>
275
+ </button>
276
+ </span>
277
+ </div>
278
+ ) : (
279
+ <input className="w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm text-slate-900 transition-all duration-150 focus:border-blue-600 focus:outline-none focus:ring-3 focus:ring-blue-600/10" type="file" accept="image/jpeg,image/png,image/gif,image/webp" onChange={(e) => {
280
+ const file = e.target.files?.[0] || null;
281
+ if (file && !["image/png","image/jpeg","image/gif","image/webp"].includes(file.type)) { e.target.value = ""; return; }
282
+ updateAnswer(question.id, file);
283
+ }} />
284
+ )}
285
+ </>
286
+ )}
287
+ </div>
288
+ ))}
289
+
290
+ <div className="flex items-center gap-3 pt-2">
291
+ <button className="inline-flex flex-1 items-center justify-center gap-2 rounded-lg border-none bg-blue-600 px-5 py-2.5 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 disabled:opacity-60 disabled:cursor-not-allowed" type="submit" disabled={isSubmitting}>
292
+ {isSubmitting ? (
293
+ <>
294
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" className="animate-spin"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
295
+ Submitting...
296
+ </>
297
+ ) : "Submit"}
298
+ </button>
299
+ </div>
300
+ </form>
301
+ </div>
302
+ )}
303
+ </div>
304
+ </div>
305
+ );
306
+ }