@jamwidgets/astro 0.1.0
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/LICENSE +21 -0
- package/README.md +250 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +15 -0
- package/dist/loader.d.ts +62 -0
- package/dist/loader.js +74 -0
- package/package.json +65 -0
- package/src/Announcements.astro +233 -0
- package/src/Comments.astro +481 -0
- package/src/Embed.astro +513 -0
- package/src/Feedback.astro +327 -0
- package/src/Form.astro +237 -0
- package/src/Poll.astro +490 -0
- package/src/Reactions.astro +317 -0
- package/src/Subscribe.astro +314 -0
- package/src/SubscribeForm.astro +154 -0
- package/src/Views.astro +203 -0
- package/src/Waitlist.astro +262 -0
- package/src/index.ts +127 -0
- package/src/loader.ts +141 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* JamWidgets Feedback Component
|
|
4
|
+
*
|
|
5
|
+
* Displays a feedback form for collecting user suggestions.
|
|
6
|
+
* Supports different feedback types (bug, feature, improvement, other).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* <Feedback siteKey={import.meta.env.JAMWIDGETS_SITE_KEY} />
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
/** Your site key (required) */
|
|
14
|
+
siteKey: string;
|
|
15
|
+
/** Base URL of your JamWidgets instance (default: 'https://jamwidgets.com') */
|
|
16
|
+
endpoint?: string;
|
|
17
|
+
/** Theme preset: 'light' (default), 'dark', or 'auto' (uses prefers-color-scheme) */
|
|
18
|
+
theme?: "light" | "dark" | "auto";
|
|
19
|
+
/** Placeholder text for the feedback input (default: 'Share your feedback...') */
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
/** Show feedback type selector (default: true) */
|
|
22
|
+
showTypeSelector?: boolean;
|
|
23
|
+
/** CSS class to add to the container */
|
|
24
|
+
class?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEFAULT_ENDPOINT = "https://jamwidgets.com";
|
|
28
|
+
const API_PATH = "/api/v1";
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
siteKey,
|
|
32
|
+
endpoint = DEFAULT_ENDPOINT,
|
|
33
|
+
theme = "light",
|
|
34
|
+
placeholder = "Share your feedback...",
|
|
35
|
+
showTypeSelector = true,
|
|
36
|
+
class: className = "",
|
|
37
|
+
} = Astro.props;
|
|
38
|
+
|
|
39
|
+
// Build the API URL
|
|
40
|
+
const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
<div
|
|
44
|
+
class:list={["jamwidgets-feedback", `jamwidgets-theme-${theme}`, className]}
|
|
45
|
+
data-jamwidgets-feedback
|
|
46
|
+
data-endpoint={baseUrl}
|
|
47
|
+
data-site-key={siteKey}
|
|
48
|
+
>
|
|
49
|
+
<form class="jamwidgets-feedback-form">
|
|
50
|
+
{showTypeSelector && (
|
|
51
|
+
<div class="jamwidgets-feedback-type-selector">
|
|
52
|
+
<label class="jamwidgets-feedback-type">
|
|
53
|
+
<input type="radio" name="feedbackType" value="feature" checked />
|
|
54
|
+
<span>Feature</span>
|
|
55
|
+
</label>
|
|
56
|
+
<label class="jamwidgets-feedback-type">
|
|
57
|
+
<input type="radio" name="feedbackType" value="bug" />
|
|
58
|
+
<span>Bug</span>
|
|
59
|
+
</label>
|
|
60
|
+
<label class="jamwidgets-feedback-type">
|
|
61
|
+
<input type="radio" name="feedbackType" value="improvement" />
|
|
62
|
+
<span>Improvement</span>
|
|
63
|
+
</label>
|
|
64
|
+
<label class="jamwidgets-feedback-type">
|
|
65
|
+
<input type="radio" name="feedbackType" value="other" />
|
|
66
|
+
<span>Other</span>
|
|
67
|
+
</label>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
<textarea
|
|
71
|
+
name="content"
|
|
72
|
+
class="jamwidgets-feedback-textarea"
|
|
73
|
+
placeholder={placeholder}
|
|
74
|
+
rows="3"
|
|
75
|
+
required
|
|
76
|
+
></textarea>
|
|
77
|
+
<div class="jamwidgets-feedback-footer">
|
|
78
|
+
<span class="jamwidgets-feedback-message"></span>
|
|
79
|
+
<button type="submit" class="jamwidgets-feedback-submit">
|
|
80
|
+
Send Feedback
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
</form>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<script>
|
|
87
|
+
// Get or create a visitor ID for anonymous users
|
|
88
|
+
const VISITOR_KEY = "jamwidgets_visitor_id";
|
|
89
|
+
function getVisitorId(): string {
|
|
90
|
+
let id = localStorage.getItem(VISITOR_KEY);
|
|
91
|
+
if (!id) {
|
|
92
|
+
id = crypto.randomUUID();
|
|
93
|
+
localStorage.setItem(VISITOR_KEY, id);
|
|
94
|
+
}
|
|
95
|
+
return id;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
document.querySelectorAll("[data-jamwidgets-feedback]").forEach((container) => {
|
|
99
|
+
const endpoint = (container as HTMLElement).dataset.endpoint;
|
|
100
|
+
const siteKey = (container as HTMLElement).dataset.siteKey;
|
|
101
|
+
|
|
102
|
+
if (!endpoint || !siteKey) return;
|
|
103
|
+
|
|
104
|
+
const visitorId = getVisitorId();
|
|
105
|
+
const headers = {
|
|
106
|
+
"X-JamWidgets-Key": siteKey,
|
|
107
|
+
"X-JamWidgets-Visitor": visitorId,
|
|
108
|
+
"Content-Type": "application/json",
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const form = container.querySelector(".jamwidgets-feedback-form") as HTMLFormElement;
|
|
112
|
+
const textarea = container.querySelector(".jamwidgets-feedback-textarea") as HTMLTextAreaElement;
|
|
113
|
+
const submitBtn = container.querySelector(".jamwidgets-feedback-submit") as HTMLButtonElement;
|
|
114
|
+
const messageEl = container.querySelector(".jamwidgets-feedback-message") as HTMLElement;
|
|
115
|
+
|
|
116
|
+
form.addEventListener("submit", async (e) => {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
|
|
119
|
+
const content = textarea.value.trim();
|
|
120
|
+
if (!content) return;
|
|
121
|
+
|
|
122
|
+
const typeInput = form.querySelector('input[name="feedbackType"]:checked') as HTMLInputElement | null;
|
|
123
|
+
const feedbackType = typeInput?.value || "other";
|
|
124
|
+
|
|
125
|
+
submitBtn.disabled = true;
|
|
126
|
+
submitBtn.textContent = "Sending...";
|
|
127
|
+
messageEl.textContent = "";
|
|
128
|
+
messageEl.className = "jamwidgets-feedback-message";
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const response = await fetch(`${endpoint}/feedback`, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers,
|
|
134
|
+
body: JSON.stringify({ content, feedbackType }),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
const error = await response.json();
|
|
139
|
+
throw new Error(error.message || "Failed to submit feedback");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
textarea.value = "";
|
|
143
|
+
messageEl.textContent = "Thank you for your feedback!";
|
|
144
|
+
messageEl.className = "jamwidgets-feedback-message jamwidgets-success";
|
|
145
|
+
|
|
146
|
+
container.dispatchEvent(
|
|
147
|
+
new CustomEvent("jamwidgets:feedback-submitted", {
|
|
148
|
+
detail: { content, feedbackType },
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
setTimeout(() => {
|
|
153
|
+
messageEl.textContent = "";
|
|
154
|
+
messageEl.className = "jamwidgets-feedback-message";
|
|
155
|
+
}, 3000);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
console.error("Failed to submit feedback:", e);
|
|
158
|
+
messageEl.textContent = e instanceof Error ? e.message : "Failed to submit feedback";
|
|
159
|
+
messageEl.className = "jamwidgets-feedback-message jamwidgets-error";
|
|
160
|
+
} finally {
|
|
161
|
+
submitBtn.disabled = false;
|
|
162
|
+
submitBtn.textContent = "Send Feedback";
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
</script>
|
|
167
|
+
|
|
168
|
+
<style is:global>
|
|
169
|
+
.jamwidgets-feedback {
|
|
170
|
+
--jamwidgets-border-color: #9ca3af;
|
|
171
|
+
--jamwidgets-bg-color: white;
|
|
172
|
+
--jamwidgets-text-color: inherit;
|
|
173
|
+
--jamwidgets-text-muted: #6b7280;
|
|
174
|
+
--jamwidgets-hover-bg: #f3f4f6;
|
|
175
|
+
--jamwidgets-selected-bg: #dbeafe;
|
|
176
|
+
--jamwidgets-selected-border: #3b82f6;
|
|
177
|
+
--jamwidgets-button-bg: #3b82f6;
|
|
178
|
+
--jamwidgets-button-text: white;
|
|
179
|
+
--jamwidgets-button-hover: #2563eb;
|
|
180
|
+
--jamwidgets-success-color: #16a34a;
|
|
181
|
+
--jamwidgets-error-color: #dc2626;
|
|
182
|
+
font-family: inherit;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.jamwidgets-feedback.jamwidgets-theme-dark {
|
|
186
|
+
--jamwidgets-border-color: #9ca3af;
|
|
187
|
+
--jamwidgets-bg-color: #374151;
|
|
188
|
+
--jamwidgets-text-color: #f3f4f6;
|
|
189
|
+
--jamwidgets-text-muted: #9ca3af;
|
|
190
|
+
--jamwidgets-hover-bg: #4b5563;
|
|
191
|
+
--jamwidgets-selected-bg: rgba(59, 130, 246, 0.3);
|
|
192
|
+
--jamwidgets-selected-border: #60a5fa;
|
|
193
|
+
--jamwidgets-success-color: #22c55e;
|
|
194
|
+
--jamwidgets-error-color: #ef4444;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@media (prefers-color-scheme: dark) {
|
|
198
|
+
.jamwidgets-feedback.jamwidgets-theme-auto {
|
|
199
|
+
--jamwidgets-border-color: #9ca3af;
|
|
200
|
+
--jamwidgets-bg-color: #374151;
|
|
201
|
+
--jamwidgets-text-color: #f3f4f6;
|
|
202
|
+
--jamwidgets-text-muted: #9ca3af;
|
|
203
|
+
--jamwidgets-hover-bg: #4b5563;
|
|
204
|
+
--jamwidgets-selected-bg: rgba(59, 130, 246, 0.3);
|
|
205
|
+
--jamwidgets-selected-border: #60a5fa;
|
|
206
|
+
--jamwidgets-success-color: #22c55e;
|
|
207
|
+
--jamwidgets-error-color: #ef4444;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.jamwidgets-feedback-form {
|
|
212
|
+
display: flex;
|
|
213
|
+
flex-direction: column;
|
|
214
|
+
gap: 0.75rem;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.jamwidgets-feedback-type-selector {
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-wrap: wrap;
|
|
220
|
+
gap: 0.5rem;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.jamwidgets-feedback-type {
|
|
224
|
+
position: relative;
|
|
225
|
+
display: inline-flex;
|
|
226
|
+
align-items: center;
|
|
227
|
+
cursor: pointer;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.jamwidgets-feedback-type input {
|
|
231
|
+
position: absolute;
|
|
232
|
+
width: 1px;
|
|
233
|
+
height: 1px;
|
|
234
|
+
padding: 0;
|
|
235
|
+
margin: -1px;
|
|
236
|
+
overflow: hidden;
|
|
237
|
+
clip: rect(0, 0, 0, 0);
|
|
238
|
+
white-space: nowrap;
|
|
239
|
+
border: 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.jamwidgets-feedback-type span {
|
|
243
|
+
display: inline-block;
|
|
244
|
+
padding: 0.375rem 0.75rem;
|
|
245
|
+
font-size: 0.875rem;
|
|
246
|
+
border: 1px solid var(--jamwidgets-border-color);
|
|
247
|
+
border-radius: 9999px;
|
|
248
|
+
transition: all 0.15s;
|
|
249
|
+
color: var(--jamwidgets-text-color);
|
|
250
|
+
background: transparent;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.jamwidgets-feedback-type:hover span {
|
|
254
|
+
background: var(--jamwidgets-hover-bg);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.jamwidgets-feedback-type input:checked + span {
|
|
258
|
+
background: var(--jamwidgets-selected-bg);
|
|
259
|
+
border-color: var(--jamwidgets-selected-border);
|
|
260
|
+
color: var(--jamwidgets-selected-border);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.jamwidgets-feedback .jamwidgets-feedback-textarea {
|
|
264
|
+
width: 100%;
|
|
265
|
+
padding: 0.75rem;
|
|
266
|
+
font-size: 0.875rem;
|
|
267
|
+
border: 1px solid var(--jamwidgets-border-color);
|
|
268
|
+
border-radius: 0.5rem;
|
|
269
|
+
background: var(--jamwidgets-bg-color);
|
|
270
|
+
color: var(--jamwidgets-text-color);
|
|
271
|
+
resize: vertical;
|
|
272
|
+
min-height: 80px;
|
|
273
|
+
font-family: inherit;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.jamwidgets-feedback .jamwidgets-feedback-textarea:focus {
|
|
277
|
+
outline: none;
|
|
278
|
+
border-color: var(--jamwidgets-selected-border);
|
|
279
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.jamwidgets-feedback .jamwidgets-feedback-textarea::placeholder {
|
|
283
|
+
color: var(--jamwidgets-text-muted);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.jamwidgets-feedback-footer {
|
|
287
|
+
display: flex;
|
|
288
|
+
justify-content: space-between;
|
|
289
|
+
align-items: center;
|
|
290
|
+
gap: 1rem;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.jamwidgets-feedback-message {
|
|
294
|
+
font-size: 0.875rem;
|
|
295
|
+
color: var(--jamwidgets-text-muted);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.jamwidgets-feedback-message.jamwidgets-success {
|
|
299
|
+
color: var(--jamwidgets-success-color);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.jamwidgets-feedback-message.jamwidgets-error {
|
|
303
|
+
color: var(--jamwidgets-error-color);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.jamwidgets-feedback .jamwidgets-feedback-submit {
|
|
307
|
+
padding: 0.5rem 1rem;
|
|
308
|
+
background: var(--jamwidgets-button-bg);
|
|
309
|
+
color: var(--jamwidgets-button-text);
|
|
310
|
+
border: none;
|
|
311
|
+
border-radius: 0.5rem;
|
|
312
|
+
font-size: 0.875rem;
|
|
313
|
+
font-weight: 500;
|
|
314
|
+
cursor: pointer;
|
|
315
|
+
transition: background 0.15s;
|
|
316
|
+
white-space: nowrap;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.jamwidgets-feedback .jamwidgets-feedback-submit:hover:not(:disabled) {
|
|
320
|
+
background: var(--jamwidgets-button-hover);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.jamwidgets-feedback .jamwidgets-feedback-submit:disabled {
|
|
324
|
+
opacity: 0.5;
|
|
325
|
+
cursor: not-allowed;
|
|
326
|
+
}
|
|
327
|
+
</style>
|
package/src/Form.astro
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* JamWidgets Form Component
|
|
4
|
+
*
|
|
5
|
+
* A wrapper component that handles form submission to JamWidgets.
|
|
6
|
+
* You provide your own form fields via the default slot.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* <Form siteKey={import.meta.env.JAMWIDGETS_SITE_KEY} formSlug="contact">
|
|
10
|
+
* <input name="name" placeholder="Name" required />
|
|
11
|
+
* <input name="email" type="email" placeholder="Email" required />
|
|
12
|
+
* <textarea name="message" placeholder="Message" required></textarea>
|
|
13
|
+
* <button type="submit">Send</button>
|
|
14
|
+
* </Form>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
/** Your site key (required) */
|
|
19
|
+
siteKey: string;
|
|
20
|
+
/** The form slug as configured in JamWidgets (required) */
|
|
21
|
+
formSlug: string;
|
|
22
|
+
/** Base URL of your JamWidgets instance (default: 'https://jamwidgets.com') */
|
|
23
|
+
endpoint?: string;
|
|
24
|
+
/** Theme preset: 'light' (default), 'dark', or 'auto' (uses prefers-color-scheme) */
|
|
25
|
+
theme?: "light" | "dark" | "auto";
|
|
26
|
+
/** CSS class to add to the form */
|
|
27
|
+
class?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEFAULT_ENDPOINT = "https://jamwidgets.com";
|
|
31
|
+
const API_PATH = "/api/v1";
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
siteKey,
|
|
35
|
+
formSlug,
|
|
36
|
+
endpoint = DEFAULT_ENDPOINT,
|
|
37
|
+
theme = "light",
|
|
38
|
+
class: className = "",
|
|
39
|
+
} = Astro.props;
|
|
40
|
+
|
|
41
|
+
// Build the API URL
|
|
42
|
+
const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
<form
|
|
46
|
+
class:list={["jamwidgets-form", `jamwidgets-theme-${theme}`, className]}
|
|
47
|
+
data-jamwidgets-form
|
|
48
|
+
data-endpoint={baseUrl}
|
|
49
|
+
data-site-key={siteKey}
|
|
50
|
+
data-form-slug={formSlug}
|
|
51
|
+
>
|
|
52
|
+
<slot />
|
|
53
|
+
|
|
54
|
+
<!-- Honeypot field for spam protection (hidden from users, catches bots) -->
|
|
55
|
+
<div style="position: absolute; left: -9999px;" aria-hidden="true">
|
|
56
|
+
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" />
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<!-- Success/Error message container (hidden by default) -->
|
|
60
|
+
<div class="jamwidgets-form-message" style="display: none;" aria-live="polite"></div>
|
|
61
|
+
</form>
|
|
62
|
+
|
|
63
|
+
<script>
|
|
64
|
+
interface FormSubmitResponse {
|
|
65
|
+
success: boolean;
|
|
66
|
+
message: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Get or create a visitor ID for anonymous users
|
|
70
|
+
const VISITOR_KEY = "jamwidgets_visitor_id";
|
|
71
|
+
function getVisitorId(): string {
|
|
72
|
+
let id = localStorage.getItem(VISITOR_KEY);
|
|
73
|
+
if (!id) {
|
|
74
|
+
id = crypto.randomUUID();
|
|
75
|
+
localStorage.setItem(VISITOR_KEY, id);
|
|
76
|
+
}
|
|
77
|
+
return id;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Store form load timestamps for spam detection
|
|
81
|
+
const formTimestamps = new WeakMap<Element, number>();
|
|
82
|
+
|
|
83
|
+
document.querySelectorAll("[data-jamwidgets-form]").forEach((form) => {
|
|
84
|
+
// Record when form was loaded (for time-based spam detection)
|
|
85
|
+
formTimestamps.set(form, Math.floor(Date.now() / 1000));
|
|
86
|
+
|
|
87
|
+
form.addEventListener("submit", async (e) => {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
|
|
90
|
+
const formEl = e.target as HTMLFormElement;
|
|
91
|
+
const endpoint = formEl.dataset.endpoint;
|
|
92
|
+
const siteKey = formEl.dataset.siteKey;
|
|
93
|
+
const formSlug = formEl.dataset.formSlug;
|
|
94
|
+
const messageEl = formEl.querySelector(".jamwidgets-form-message") as HTMLElement | null;
|
|
95
|
+
const submitBtn = formEl.querySelector('[type="submit"]') as HTMLButtonElement | null;
|
|
96
|
+
|
|
97
|
+
if (!endpoint || !siteKey || !formSlug) {
|
|
98
|
+
const missing = [];
|
|
99
|
+
if (!endpoint) missing.push("endpoint");
|
|
100
|
+
if (!formSlug) missing.push("formSlug");
|
|
101
|
+
if (!siteKey) {
|
|
102
|
+
missing.push("siteKey (check JAMWIDGETS_SITE_KEY in your .env)");
|
|
103
|
+
}
|
|
104
|
+
console.error(`JamWidgets form missing required props: ${missing.join(", ")}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Collect form data
|
|
109
|
+
const formData = new FormData(formEl);
|
|
110
|
+
const data: Record<string, unknown> = {};
|
|
111
|
+
formData.forEach((value, key) => {
|
|
112
|
+
data[key] = value;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Add timestamp for time-based spam detection
|
|
116
|
+
const loadTimestamp = formTimestamps.get(formEl);
|
|
117
|
+
if (loadTimestamp) {
|
|
118
|
+
data._jamwidgets_ts = loadTimestamp;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Dispatch loading event
|
|
122
|
+
formEl.dispatchEvent(new CustomEvent("jamwidgets:loading"));
|
|
123
|
+
|
|
124
|
+
// Disable submit button
|
|
125
|
+
if (submitBtn) {
|
|
126
|
+
submitBtn.disabled = true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const response = await fetch(`${endpoint}/forms/${formSlug}/submit`, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: {
|
|
133
|
+
"Content-Type": "application/json",
|
|
134
|
+
"X-JamWidgets-Key": siteKey,
|
|
135
|
+
"X-JamWidgets-Visitor": getVisitorId(),
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify(data),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
const errorText = await response.text();
|
|
142
|
+
throw new Error(errorText || "Submission failed");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const result: FormSubmitResponse = await response.json();
|
|
146
|
+
|
|
147
|
+
// Show success message
|
|
148
|
+
if (messageEl) {
|
|
149
|
+
messageEl.textContent = result.message;
|
|
150
|
+
messageEl.style.display = "block";
|
|
151
|
+
messageEl.className = "jamwidgets-form-message jamwidgets-form-success";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Dispatch success event with response data
|
|
155
|
+
formEl.dispatchEvent(
|
|
156
|
+
new CustomEvent("jamwidgets:success", {
|
|
157
|
+
detail: result,
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Reset form on success
|
|
162
|
+
formEl.reset();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
// Show error message
|
|
165
|
+
if (messageEl) {
|
|
166
|
+
messageEl.textContent = error instanceof Error ? error.message : "Something went wrong";
|
|
167
|
+
messageEl.style.display = "block";
|
|
168
|
+
messageEl.className = "jamwidgets-form-message jamwidgets-form-error";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
formEl.dispatchEvent(
|
|
172
|
+
new CustomEvent("jamwidgets:error", {
|
|
173
|
+
detail: error,
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
} finally {
|
|
177
|
+
// Re-enable submit button
|
|
178
|
+
if (submitBtn) {
|
|
179
|
+
submitBtn.disabled = false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<style is:global>
|
|
187
|
+
@layer jamwidgets {
|
|
188
|
+
.jamwidgets-form {
|
|
189
|
+
--jamwidgets-success-bg: #dcfce7;
|
|
190
|
+
--jamwidgets-success-text: #166534;
|
|
191
|
+
--jamwidgets-success-border: #bbf7d0;
|
|
192
|
+
--jamwidgets-error-bg: #fef2f2;
|
|
193
|
+
--jamwidgets-error-text: #991b1b;
|
|
194
|
+
--jamwidgets-error-border: #fecaca;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Dark theme */
|
|
198
|
+
.jamwidgets-form.jamwidgets-theme-dark {
|
|
199
|
+
--jamwidgets-success-bg: rgba(34, 197, 94, 0.15);
|
|
200
|
+
--jamwidgets-success-text: #86efac;
|
|
201
|
+
--jamwidgets-success-border: rgba(34, 197, 94, 0.3);
|
|
202
|
+
--jamwidgets-error-bg: rgba(239, 68, 68, 0.15);
|
|
203
|
+
--jamwidgets-error-text: #fca5a5;
|
|
204
|
+
--jamwidgets-error-border: rgba(239, 68, 68, 0.3);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* Auto theme - follows prefers-color-scheme */
|
|
208
|
+
@media (prefers-color-scheme: dark) {
|
|
209
|
+
.jamwidgets-form.jamwidgets-theme-auto {
|
|
210
|
+
--jamwidgets-success-bg: rgba(34, 197, 94, 0.15);
|
|
211
|
+
--jamwidgets-success-text: #86efac;
|
|
212
|
+
--jamwidgets-success-border: rgba(34, 197, 94, 0.3);
|
|
213
|
+
--jamwidgets-error-bg: rgba(239, 68, 68, 0.15);
|
|
214
|
+
--jamwidgets-error-text: #fca5a5;
|
|
215
|
+
--jamwidgets-error-border: rgba(239, 68, 68, 0.3);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.jamwidgets-form-message {
|
|
220
|
+
margin-top: 1rem;
|
|
221
|
+
padding: 0.75rem 1rem;
|
|
222
|
+
border-radius: 0.375rem;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.jamwidgets-form-success {
|
|
226
|
+
background-color: var(--jamwidgets-success-bg);
|
|
227
|
+
color: var(--jamwidgets-success-text);
|
|
228
|
+
border: 1px solid var(--jamwidgets-success-border);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.jamwidgets-form-error {
|
|
232
|
+
background-color: var(--jamwidgets-error-bg);
|
|
233
|
+
color: var(--jamwidgets-error-text);
|
|
234
|
+
border: 1px solid var(--jamwidgets-error-border);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
</style>
|