@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,154 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* JamWidgets Headless Subscribe Form
|
|
4
|
+
*
|
|
5
|
+
* A headless subscription form that handles API/state but lets you provide your own UI.
|
|
6
|
+
* Use the default slot to provide your own email input and submit button.
|
|
7
|
+
*
|
|
8
|
+
* Required: An input with name="email" and a submit button.
|
|
9
|
+
*
|
|
10
|
+
* @example Basic usage
|
|
11
|
+
* <SubscribeForm siteKey={import.meta.env.JAMWIDGETS_SITE_KEY}>
|
|
12
|
+
* <input type="email" name="email" placeholder="you@example.com" required />
|
|
13
|
+
* <button type="submit">Subscribe</button>
|
|
14
|
+
* </SubscribeForm>
|
|
15
|
+
*
|
|
16
|
+
* @example With custom message handling
|
|
17
|
+
* <SubscribeForm siteKey={import.meta.env.JAMWIDGETS_SITE_KEY} id="my-form">
|
|
18
|
+
* <input type="email" name="email" class="my-input" required />
|
|
19
|
+
* <button type="submit">Get updates</button>
|
|
20
|
+
* <p class="message"></p>
|
|
21
|
+
* </SubscribeForm>
|
|
22
|
+
* <script>
|
|
23
|
+
* document.getElementById('my-form')?.addEventListener('jamwidgets:success', (e) => {
|
|
24
|
+
* e.target.querySelector('.message').textContent = e.detail.message;
|
|
25
|
+
* });
|
|
26
|
+
* </script>
|
|
27
|
+
*
|
|
28
|
+
* Events:
|
|
29
|
+
* - jamwidgets:loading - Fired when submission starts
|
|
30
|
+
* - jamwidgets:success - Fired on success, detail contains { success, message }
|
|
31
|
+
* - jamwidgets:error - Fired on error, detail contains the Error object
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
interface Props {
|
|
35
|
+
/** Your site key (required) */
|
|
36
|
+
siteKey: string;
|
|
37
|
+
/** Base URL of your JamWidgets instance (default: 'https://jamwidgets.com') */
|
|
38
|
+
endpoint?: string;
|
|
39
|
+
/** HTML id attribute for the form */
|
|
40
|
+
id?: string;
|
|
41
|
+
/** CSS class for the form */
|
|
42
|
+
class?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const DEFAULT_ENDPOINT = "https://jamwidgets.com";
|
|
46
|
+
const API_PATH = "/api/v1";
|
|
47
|
+
|
|
48
|
+
const {
|
|
49
|
+
siteKey,
|
|
50
|
+
endpoint = DEFAULT_ENDPOINT,
|
|
51
|
+
id,
|
|
52
|
+
class: className = "",
|
|
53
|
+
} = Astro.props;
|
|
54
|
+
|
|
55
|
+
const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
<form
|
|
59
|
+
id={id}
|
|
60
|
+
class:list={["jamwidgets-subscribe-form", className]}
|
|
61
|
+
data-jamwidgets-subscribe-form
|
|
62
|
+
data-endpoint={baseUrl}
|
|
63
|
+
data-site-key={siteKey}
|
|
64
|
+
>
|
|
65
|
+
<slot />
|
|
66
|
+
|
|
67
|
+
<!-- Honeypot field for spam protection -->
|
|
68
|
+
<div style="position: absolute; left: -9999px;" aria-hidden="true">
|
|
69
|
+
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" />
|
|
70
|
+
</div>
|
|
71
|
+
</form>
|
|
72
|
+
|
|
73
|
+
<script>
|
|
74
|
+
interface SubscribeResponse {
|
|
75
|
+
success: boolean;
|
|
76
|
+
message: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Get or create a visitor ID for anonymous users
|
|
80
|
+
const VISITOR_KEY = "jamwidgets_visitor_id";
|
|
81
|
+
function getVisitorId(): string {
|
|
82
|
+
let id = localStorage.getItem(VISITOR_KEY);
|
|
83
|
+
if (!id) {
|
|
84
|
+
id = crypto.randomUUID();
|
|
85
|
+
localStorage.setItem(VISITOR_KEY, id);
|
|
86
|
+
}
|
|
87
|
+
return id;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
document.querySelectorAll("[data-jamwidgets-subscribe-form]").forEach((form) => {
|
|
91
|
+
form.addEventListener("submit", async (e) => {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
|
|
94
|
+
const formEl = e.target as HTMLFormElement;
|
|
95
|
+
const endpoint = formEl.dataset.endpoint;
|
|
96
|
+
const siteKey = formEl.dataset.siteKey;
|
|
97
|
+
|
|
98
|
+
if (!endpoint || !siteKey) {
|
|
99
|
+
console.error("JamWidgets SubscribeForm: missing siteKey or endpoint");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Collect form data
|
|
104
|
+
const formData = new FormData(formEl);
|
|
105
|
+
const data: Record<string, unknown> = {};
|
|
106
|
+
formData.forEach((value, key) => {
|
|
107
|
+
data[key] = value;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Dispatch loading event
|
|
111
|
+
formEl.dispatchEvent(new CustomEvent("jamwidgets:loading", { bubbles: true }));
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const response = await fetch(`${endpoint}/subscribe`, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: {
|
|
117
|
+
"Content-Type": "application/json",
|
|
118
|
+
"X-JamWidgets-Key": siteKey,
|
|
119
|
+
"X-JamWidgets-Visitor": getVisitorId(),
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify(data),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
const errorText = await response.text();
|
|
126
|
+
throw new Error(errorText || "Subscription failed");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const result: SubscribeResponse = await response.json();
|
|
130
|
+
|
|
131
|
+
// Dispatch success event
|
|
132
|
+
formEl.dispatchEvent(
|
|
133
|
+
new CustomEvent("jamwidgets:success", {
|
|
134
|
+
bubbles: true,
|
|
135
|
+
detail: result,
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Clear email input on success
|
|
140
|
+
const emailInput = formEl.querySelector('input[name="email"]') as HTMLInputElement | null;
|
|
141
|
+
if (emailInput) {
|
|
142
|
+
emailInput.value = "";
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
formEl.dispatchEvent(
|
|
146
|
+
new CustomEvent("jamwidgets:error", {
|
|
147
|
+
bubbles: true,
|
|
148
|
+
detail: error,
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
</script>
|
package/src/Views.astro
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* JamWidgets Views Component
|
|
4
|
+
*
|
|
5
|
+
* Tracks page views and displays view count.
|
|
6
|
+
* Automatically records a view when the page loads.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* <Views
|
|
10
|
+
* siteKey={import.meta.env.JAMWIDGETS_SITE_KEY}
|
|
11
|
+
* pageId={Astro.url.pathname}
|
|
12
|
+
* />
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
/** Your site key (required) */
|
|
17
|
+
siteKey: string;
|
|
18
|
+
/** Unique identifier for this page (e.g., slug or URL path) */
|
|
19
|
+
pageId: string;
|
|
20
|
+
/** Base URL of your JamWidgets instance (default: 'https://jamwidgets.com') */
|
|
21
|
+
endpoint?: string;
|
|
22
|
+
/** Show unique visitor count alongside total views (default: false) */
|
|
23
|
+
showUnique?: boolean;
|
|
24
|
+
/** Theme preset: 'light' (default), 'dark', or 'auto' (uses prefers-color-scheme) */
|
|
25
|
+
theme?: "light" | "dark" | "auto";
|
|
26
|
+
/** CSS class to add to the container */
|
|
27
|
+
class?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEFAULT_ENDPOINT = "https://jamwidgets.com";
|
|
31
|
+
const API_PATH = "/api/v1";
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
siteKey,
|
|
35
|
+
pageId,
|
|
36
|
+
endpoint = DEFAULT_ENDPOINT,
|
|
37
|
+
showUnique = false,
|
|
38
|
+
theme = "light",
|
|
39
|
+
class: className = "",
|
|
40
|
+
} = Astro.props;
|
|
41
|
+
|
|
42
|
+
// Build the API URL
|
|
43
|
+
const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
<div
|
|
47
|
+
class:list={["jamwidgets-views", `jamwidgets-theme-${theme}`, className]}
|
|
48
|
+
data-jamwidgets-views
|
|
49
|
+
data-endpoint={baseUrl}
|
|
50
|
+
data-site-key={siteKey}
|
|
51
|
+
data-page-id={pageId}
|
|
52
|
+
data-show-unique={showUnique}
|
|
53
|
+
>
|
|
54
|
+
<span class="jamwidgets-views-icon">
|
|
55
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
56
|
+
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/>
|
|
57
|
+
<circle cx="12" cy="12" r="3"/>
|
|
58
|
+
</svg>
|
|
59
|
+
</span>
|
|
60
|
+
<span class="jamwidgets-views-count">-</span>
|
|
61
|
+
<span class="jamwidgets-views-unique" style="display: none;"></span>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<script>
|
|
65
|
+
// Get or create a visitor ID for anonymous users
|
|
66
|
+
const VISITOR_KEY = "jamwidgets_visitor_id";
|
|
67
|
+
function getVisitorId(): string {
|
|
68
|
+
let id = localStorage.getItem(VISITOR_KEY);
|
|
69
|
+
if (!id) {
|
|
70
|
+
id = crypto.randomUUID();
|
|
71
|
+
localStorage.setItem(VISITOR_KEY, id);
|
|
72
|
+
}
|
|
73
|
+
return id;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
document.querySelectorAll("[data-jamwidgets-views]").forEach((container) => {
|
|
77
|
+
const endpoint = (container as HTMLElement).dataset.endpoint;
|
|
78
|
+
const siteKey = (container as HTMLElement).dataset.siteKey;
|
|
79
|
+
const pageId = (container as HTMLElement).dataset.pageId;
|
|
80
|
+
const showUnique = (container as HTMLElement).dataset.showUnique === "true";
|
|
81
|
+
|
|
82
|
+
if (!endpoint || !siteKey || !pageId) return;
|
|
83
|
+
|
|
84
|
+
const visitorId = getVisitorId();
|
|
85
|
+
const headers = {
|
|
86
|
+
"X-JamWidgets-Key": siteKey,
|
|
87
|
+
"X-JamWidgets-Visitor": visitorId,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const countEl = container.querySelector(".jamwidgets-views-count");
|
|
91
|
+
const uniqueEl = container.querySelector(".jamwidgets-views-unique") as HTMLElement;
|
|
92
|
+
|
|
93
|
+
// Record the view
|
|
94
|
+
async function recordView() {
|
|
95
|
+
try {
|
|
96
|
+
const response = await fetch(
|
|
97
|
+
`${endpoint}/views/${encodeURIComponent(pageId!)}`,
|
|
98
|
+
{
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers,
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
if (!response.ok) return;
|
|
104
|
+
const data = await response.json();
|
|
105
|
+
const result = data.record_view_response;
|
|
106
|
+
|
|
107
|
+
if (countEl) {
|
|
108
|
+
countEl.textContent = String(result?.views || 0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (showUnique && uniqueEl && result?.unique_visitors !== undefined) {
|
|
112
|
+
uniqueEl.textContent = `(${result.unique_visitors} unique)`;
|
|
113
|
+
uniqueEl.style.display = "";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
container.dispatchEvent(
|
|
117
|
+
new CustomEvent("jamwidgets:view-recorded", {
|
|
118
|
+
detail: result,
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.error("Failed to record view:", e);
|
|
123
|
+
// Fallback: try to load existing counts
|
|
124
|
+
loadView();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Load view count without recording
|
|
129
|
+
async function loadView() {
|
|
130
|
+
try {
|
|
131
|
+
const response = await fetch(
|
|
132
|
+
`${endpoint}/views/${encodeURIComponent(pageId!)}`,
|
|
133
|
+
{ headers },
|
|
134
|
+
);
|
|
135
|
+
if (!response.ok) return;
|
|
136
|
+
const data = await response.json();
|
|
137
|
+
const result = data.page_view_counts;
|
|
138
|
+
|
|
139
|
+
if (countEl) {
|
|
140
|
+
countEl.textContent = String(result?.views || 0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (showUnique && uniqueEl && result?.uniqueVisitors !== undefined) {
|
|
144
|
+
uniqueEl.textContent = `(${result.uniqueVisitors} unique)`;
|
|
145
|
+
uniqueEl.style.display = "";
|
|
146
|
+
}
|
|
147
|
+
} catch (e) {
|
|
148
|
+
console.error("Failed to load view:", e);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Record the view on page load
|
|
153
|
+
recordView();
|
|
154
|
+
});
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
<style is:global>
|
|
158
|
+
@layer jamwidgets {
|
|
159
|
+
.jamwidgets-views {
|
|
160
|
+
--jamwidgets-text-color: #6b7280;
|
|
161
|
+
--jamwidgets-icon-color: #9ca3af;
|
|
162
|
+
|
|
163
|
+
display: inline-flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
gap: 0.375rem;
|
|
166
|
+
font-size: 0.875rem;
|
|
167
|
+
color: var(--jamwidgets-text-color);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* Dark theme */
|
|
171
|
+
.jamwidgets-views.jamwidgets-theme-dark {
|
|
172
|
+
--jamwidgets-text-color: #9ca3af;
|
|
173
|
+
--jamwidgets-icon-color: #6b7280;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* Auto theme - follows prefers-color-scheme */
|
|
177
|
+
@media (prefers-color-scheme: dark) {
|
|
178
|
+
.jamwidgets-views.jamwidgets-theme-auto {
|
|
179
|
+
--jamwidgets-text-color: #9ca3af;
|
|
180
|
+
--jamwidgets-icon-color: #6b7280;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.jamwidgets-views-icon {
|
|
185
|
+
display: flex;
|
|
186
|
+
color: var(--jamwidgets-icon-color);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.jamwidgets-views-icon svg {
|
|
190
|
+
width: 1em;
|
|
191
|
+
height: 1em;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.jamwidgets-views-count {
|
|
195
|
+
font-variant-numeric: tabular-nums;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.jamwidgets-views-unique {
|
|
199
|
+
color: var(--jamwidgets-icon-color);
|
|
200
|
+
font-size: 0.75rem;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
</style>
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* JamWidgets Waitlist Component
|
|
4
|
+
*
|
|
5
|
+
* Displays a waitlist signup form for collecting emails before launch.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <Waitlist siteKey={import.meta.env.JAMWIDGETS_SITE_KEY} />
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** Your site key (required) */
|
|
13
|
+
siteKey: string;
|
|
14
|
+
/** Base URL of your JamWidgets instance (default: 'https://jamwidgets.com') */
|
|
15
|
+
endpoint?: string;
|
|
16
|
+
/** Theme preset: 'light' (default), 'dark', or 'auto' (uses prefers-color-scheme) */
|
|
17
|
+
theme?: "light" | "dark" | "auto";
|
|
18
|
+
/** Placeholder text for the email input (default: 'Enter your email') */
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
/** Button text (default: 'Join Waitlist') */
|
|
21
|
+
buttonText?: string;
|
|
22
|
+
/** Success message (default: "You're on the list!") */
|
|
23
|
+
successMessage?: string;
|
|
24
|
+
/** CSS class to add to the container */
|
|
25
|
+
class?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULT_ENDPOINT = "https://jamwidgets.com";
|
|
29
|
+
const API_PATH = "/api/v1";
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
siteKey,
|
|
33
|
+
endpoint = DEFAULT_ENDPOINT,
|
|
34
|
+
theme = "light",
|
|
35
|
+
placeholder = "Enter your email",
|
|
36
|
+
buttonText = "Join Waitlist",
|
|
37
|
+
successMessage = "You're on the list!",
|
|
38
|
+
class: className = "",
|
|
39
|
+
} = Astro.props;
|
|
40
|
+
|
|
41
|
+
// Build the API URL
|
|
42
|
+
const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
<div
|
|
46
|
+
class:list={["jamwidgets-waitlist", `jamwidgets-theme-${theme}`, className]}
|
|
47
|
+
data-jamwidgets-waitlist
|
|
48
|
+
data-endpoint={baseUrl}
|
|
49
|
+
data-site-key={siteKey}
|
|
50
|
+
data-success-message={successMessage}
|
|
51
|
+
>
|
|
52
|
+
<form class="jamwidgets-waitlist-form">
|
|
53
|
+
<input
|
|
54
|
+
type="email"
|
|
55
|
+
name="email"
|
|
56
|
+
class="jamwidgets-waitlist-input"
|
|
57
|
+
placeholder={placeholder}
|
|
58
|
+
required
|
|
59
|
+
/>
|
|
60
|
+
<button type="submit" class="jamwidgets-waitlist-submit">
|
|
61
|
+
{buttonText}
|
|
62
|
+
</button>
|
|
63
|
+
</form>
|
|
64
|
+
<div class="jamwidgets-waitlist-message" style="display: none;"></div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<script>
|
|
68
|
+
// Get or create a visitor ID for anonymous users
|
|
69
|
+
const VISITOR_KEY = "jamwidgets_visitor_id";
|
|
70
|
+
function getVisitorId(): string {
|
|
71
|
+
let id = localStorage.getItem(VISITOR_KEY);
|
|
72
|
+
if (!id) {
|
|
73
|
+
id = crypto.randomUUID();
|
|
74
|
+
localStorage.setItem(VISITOR_KEY, id);
|
|
75
|
+
}
|
|
76
|
+
return id;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
document.querySelectorAll("[data-jamwidgets-waitlist]").forEach((container) => {
|
|
80
|
+
const endpoint = (container as HTMLElement).dataset.endpoint;
|
|
81
|
+
const siteKey = (container as HTMLElement).dataset.siteKey;
|
|
82
|
+
const successMessage = (container as HTMLElement).dataset.successMessage || "You're on the list!";
|
|
83
|
+
|
|
84
|
+
if (!endpoint || !siteKey) return;
|
|
85
|
+
|
|
86
|
+
const visitorId = getVisitorId();
|
|
87
|
+
const headers = {
|
|
88
|
+
"X-JamWidgets-Key": siteKey,
|
|
89
|
+
"X-JamWidgets-Visitor": visitorId,
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const form = container.querySelector(".jamwidgets-waitlist-form") as HTMLFormElement;
|
|
94
|
+
const input = container.querySelector(".jamwidgets-waitlist-input") as HTMLInputElement;
|
|
95
|
+
const submitBtn = container.querySelector(".jamwidgets-waitlist-submit") as HTMLButtonElement;
|
|
96
|
+
const messageEl = container.querySelector(".jamwidgets-waitlist-message") as HTMLElement;
|
|
97
|
+
|
|
98
|
+
form.addEventListener("submit", async (e) => {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
|
|
101
|
+
const email = input.value.trim();
|
|
102
|
+
if (!email) return;
|
|
103
|
+
|
|
104
|
+
submitBtn.disabled = true;
|
|
105
|
+
const originalText = submitBtn.textContent;
|
|
106
|
+
submitBtn.textContent = "Joining...";
|
|
107
|
+
messageEl.style.display = "none";
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const response = await fetch(`${endpoint}/waitlist`, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers,
|
|
113
|
+
body: JSON.stringify({ email }),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
const error = await response.json();
|
|
118
|
+
throw new Error(error.message || "Failed to join waitlist");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Hide form and show success
|
|
122
|
+
form.style.display = "none";
|
|
123
|
+
messageEl.textContent = successMessage;
|
|
124
|
+
messageEl.className = "jamwidgets-waitlist-message jamwidgets-success";
|
|
125
|
+
messageEl.style.display = "block";
|
|
126
|
+
|
|
127
|
+
container.dispatchEvent(
|
|
128
|
+
new CustomEvent("jamwidgets:waitlist-joined", {
|
|
129
|
+
detail: { email },
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
} catch (e) {
|
|
133
|
+
console.error("Failed to join waitlist:", e);
|
|
134
|
+
messageEl.textContent = e instanceof Error ? e.message : "Failed to join waitlist";
|
|
135
|
+
messageEl.className = "jamwidgets-waitlist-message jamwidgets-error";
|
|
136
|
+
messageEl.style.display = "block";
|
|
137
|
+
submitBtn.disabled = false;
|
|
138
|
+
submitBtn.textContent = originalText;
|
|
139
|
+
|
|
140
|
+
setTimeout(() => {
|
|
141
|
+
messageEl.style.display = "none";
|
|
142
|
+
}, 3000);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<style is:global>
|
|
149
|
+
.jamwidgets-waitlist {
|
|
150
|
+
--jamwidgets-border-color: #9ca3af;
|
|
151
|
+
--jamwidgets-bg-color: white;
|
|
152
|
+
--jamwidgets-text-color: inherit;
|
|
153
|
+
--jamwidgets-text-muted: #6b7280;
|
|
154
|
+
--jamwidgets-button-bg: #3b82f6;
|
|
155
|
+
--jamwidgets-button-text: white;
|
|
156
|
+
--jamwidgets-button-hover: #2563eb;
|
|
157
|
+
--jamwidgets-success-color: #16a34a;
|
|
158
|
+
--jamwidgets-success-bg: #dcfce7;
|
|
159
|
+
--jamwidgets-error-color: #dc2626;
|
|
160
|
+
--jamwidgets-error-bg: #fef2f2;
|
|
161
|
+
font-family: inherit;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.jamwidgets-waitlist.jamwidgets-theme-dark {
|
|
165
|
+
--jamwidgets-border-color: #9ca3af;
|
|
166
|
+
--jamwidgets-bg-color: #374151;
|
|
167
|
+
--jamwidgets-text-color: #f3f4f6;
|
|
168
|
+
--jamwidgets-text-muted: #9ca3af;
|
|
169
|
+
--jamwidgets-success-color: #22c55e;
|
|
170
|
+
--jamwidgets-success-bg: rgba(34, 197, 94, 0.15);
|
|
171
|
+
--jamwidgets-error-color: #ef4444;
|
|
172
|
+
--jamwidgets-error-bg: rgba(239, 68, 68, 0.15);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@media (prefers-color-scheme: dark) {
|
|
176
|
+
.jamwidgets-waitlist.jamwidgets-theme-auto {
|
|
177
|
+
--jamwidgets-border-color: #9ca3af;
|
|
178
|
+
--jamwidgets-bg-color: #374151;
|
|
179
|
+
--jamwidgets-text-color: #f3f4f6;
|
|
180
|
+
--jamwidgets-text-muted: #9ca3af;
|
|
181
|
+
--jamwidgets-success-color: #22c55e;
|
|
182
|
+
--jamwidgets-success-bg: rgba(34, 197, 94, 0.15);
|
|
183
|
+
--jamwidgets-error-color: #ef4444;
|
|
184
|
+
--jamwidgets-error-bg: rgba(239, 68, 68, 0.15);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.jamwidgets-waitlist-form {
|
|
189
|
+
display: flex;
|
|
190
|
+
gap: 0.5rem;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.jamwidgets-waitlist .jamwidgets-waitlist-input {
|
|
194
|
+
flex: 1;
|
|
195
|
+
padding: 0.625rem 0.875rem;
|
|
196
|
+
font-size: 0.875rem;
|
|
197
|
+
border: 1px solid var(--jamwidgets-border-color);
|
|
198
|
+
border-radius: 0.5rem;
|
|
199
|
+
background: var(--jamwidgets-bg-color);
|
|
200
|
+
color: var(--jamwidgets-text-color);
|
|
201
|
+
font-family: inherit;
|
|
202
|
+
min-width: 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.jamwidgets-waitlist .jamwidgets-waitlist-input:focus {
|
|
206
|
+
outline: none;
|
|
207
|
+
border-color: var(--jamwidgets-button-bg);
|
|
208
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.jamwidgets-waitlist .jamwidgets-waitlist-input::placeholder {
|
|
212
|
+
color: var(--jamwidgets-text-muted);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.jamwidgets-waitlist .jamwidgets-waitlist-submit {
|
|
216
|
+
padding: 0.625rem 1rem;
|
|
217
|
+
background: var(--jamwidgets-button-bg);
|
|
218
|
+
color: var(--jamwidgets-button-text);
|
|
219
|
+
border: none;
|
|
220
|
+
border-radius: 0.5rem;
|
|
221
|
+
font-size: 0.875rem;
|
|
222
|
+
font-weight: 500;
|
|
223
|
+
cursor: pointer;
|
|
224
|
+
transition: background 0.15s;
|
|
225
|
+
white-space: nowrap;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.jamwidgets-waitlist .jamwidgets-waitlist-submit:hover:not(:disabled) {
|
|
229
|
+
background: var(--jamwidgets-button-hover);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.jamwidgets-waitlist .jamwidgets-waitlist-submit:disabled {
|
|
233
|
+
opacity: 0.5;
|
|
234
|
+
cursor: not-allowed;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.jamwidgets-waitlist-message {
|
|
238
|
+
padding: 0.75rem 1rem;
|
|
239
|
+
font-size: 0.875rem;
|
|
240
|
+
border-radius: 0.5rem;
|
|
241
|
+
text-align: center;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.jamwidgets-waitlist-message.jamwidgets-success {
|
|
245
|
+
background: var(--jamwidgets-success-bg);
|
|
246
|
+
color: var(--jamwidgets-success-color);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.jamwidgets-waitlist-message.jamwidgets-error {
|
|
250
|
+
background: var(--jamwidgets-error-bg);
|
|
251
|
+
color: var(--jamwidgets-error-color);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@media (max-width: 400px) {
|
|
255
|
+
.jamwidgets-waitlist-form {
|
|
256
|
+
flex-direction: column;
|
|
257
|
+
}
|
|
258
|
+
.jamwidgets-waitlist .jamwidgets-waitlist-submit {
|
|
259
|
+
width: 100%;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
</style>
|