@seriphxyz/astro 0.1.2 → 0.1.5

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.
@@ -0,0 +1,302 @@
1
+ ---
2
+ /**
3
+ * Seriph Subscribe Component
4
+ *
5
+ * A subscription form for email updates. Implements double opt-in -
6
+ * users receive a confirmation email and must click to confirm.
7
+ *
8
+ * @example
9
+ * <Subscribe siteKey={import.meta.env.SERIPH_SITE_KEY} />
10
+ *
11
+ * @example With custom styling
12
+ * <Subscribe
13
+ * siteKey={import.meta.env.SERIPH_SITE_KEY}
14
+ * buttonText="Get Updates"
15
+ * placeholder="Enter your email"
16
+ * successMessage="Check your inbox!"
17
+ * />
18
+ */
19
+
20
+ interface Props {
21
+ /** Your site key (required) */
22
+ siteKey: string;
23
+ /** Base URL of your Seriph instance (default: 'https://seriph.xyz') */
24
+ endpoint?: string;
25
+ /** Submit button text (default: 'Subscribe') */
26
+ buttonText?: string;
27
+ /** Email input placeholder (default: 'your@email.com') */
28
+ placeholder?: string;
29
+ /** Success message override (default: uses server response) */
30
+ successMessage?: string;
31
+ /** Theme preset: 'light' (default), 'dark', or 'auto' (uses prefers-color-scheme) */
32
+ theme?: "light" | "dark" | "auto";
33
+ /** CSS class to add to the form */
34
+ class?: string;
35
+ }
36
+
37
+ const DEFAULT_ENDPOINT = "https://seriph.xyz";
38
+ const API_PATH = "/api/v1";
39
+
40
+ const {
41
+ siteKey,
42
+ endpoint = DEFAULT_ENDPOINT,
43
+ buttonText = "Subscribe",
44
+ placeholder = "your@email.com",
45
+ successMessage,
46
+ theme = "light",
47
+ class: className = "",
48
+ } = Astro.props;
49
+
50
+ // Build the API URL
51
+ const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
52
+ ---
53
+
54
+ <form
55
+ class:list={["seriph-subscribe", `seriph-theme-${theme}`, className]}
56
+ data-seriph-subscribe
57
+ data-endpoint={baseUrl}
58
+ data-site-key={siteKey}
59
+ data-success-message={successMessage}
60
+ >
61
+ <div class="seriph-subscribe-input-group">
62
+ <input
63
+ type="email"
64
+ name="email"
65
+ required
66
+ placeholder={placeholder}
67
+ class="seriph-subscribe-input"
68
+ autocomplete="email"
69
+ />
70
+ <button type="submit" class="seriph-subscribe-button">
71
+ {buttonText}
72
+ </button>
73
+ </div>
74
+
75
+ <!-- Honeypot field for spam protection (hidden from users, catches bots) -->
76
+ <div style="position: absolute; left: -9999px;" aria-hidden="true">
77
+ <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" />
78
+ </div>
79
+
80
+ <!-- Success/Error message container (hidden by default) -->
81
+ <div class="seriph-subscribe-message" style="display: none;" aria-live="polite"></div>
82
+ </form>
83
+
84
+ <script>
85
+ interface SubscribeResponse {
86
+ success: boolean;
87
+ message: string;
88
+ }
89
+
90
+ document.querySelectorAll("[data-seriph-subscribe]").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
+ const customSuccessMessage = formEl.dataset.successMessage;
98
+ const messageEl = formEl.querySelector(".seriph-subscribe-message") as HTMLElement | null;
99
+ const submitBtn = formEl.querySelector('button[type="submit"]') as HTMLButtonElement | null;
100
+ const inputEl = formEl.querySelector('input[name="email"]') as HTMLInputElement | null;
101
+
102
+ if (!endpoint || !siteKey) {
103
+ console.error("Seriph Subscribe: missing siteKey or endpoint");
104
+ return;
105
+ }
106
+
107
+ // Collect form data
108
+ const formData = new FormData(formEl);
109
+ const data: Record<string, unknown> = {};
110
+ formData.forEach((value, key) => {
111
+ data[key] = value;
112
+ });
113
+
114
+ // Dispatch loading event
115
+ formEl.dispatchEvent(new CustomEvent("seriph:loading"));
116
+
117
+ // Disable submit button and show loading state
118
+ if (submitBtn) {
119
+ submitBtn.disabled = true;
120
+ submitBtn.dataset.originalText = submitBtn.textContent || "";
121
+ submitBtn.textContent = "...";
122
+ }
123
+
124
+ try {
125
+ const response = await fetch(`${endpoint}/subscribe`, {
126
+ method: "POST",
127
+ headers: {
128
+ "Content-Type": "application/json",
129
+ "X-Seriph-Key": siteKey,
130
+ },
131
+ body: JSON.stringify(data),
132
+ });
133
+
134
+ if (!response.ok) {
135
+ const errorText = await response.text();
136
+ throw new Error(errorText || "Subscription failed");
137
+ }
138
+
139
+ const result: SubscribeResponse = await response.json();
140
+
141
+ // Show success message
142
+ if (messageEl) {
143
+ messageEl.textContent = customSuccessMessage || result.message;
144
+ messageEl.style.display = "block";
145
+ messageEl.className = "seriph-subscribe-message seriph-subscribe-success";
146
+ }
147
+
148
+ // Dispatch success event with response data
149
+ formEl.dispatchEvent(
150
+ new CustomEvent("seriph:success", {
151
+ detail: result,
152
+ }),
153
+ );
154
+
155
+ // Clear input on success
156
+ if (inputEl) {
157
+ inputEl.value = "";
158
+ }
159
+ } catch (error) {
160
+ // Show error message
161
+ if (messageEl) {
162
+ messageEl.textContent = error instanceof Error ? error.message : "Something went wrong";
163
+ messageEl.style.display = "block";
164
+ messageEl.className = "seriph-subscribe-message seriph-subscribe-error";
165
+ }
166
+
167
+ formEl.dispatchEvent(
168
+ new CustomEvent("seriph:error", {
169
+ detail: error,
170
+ }),
171
+ );
172
+ } finally {
173
+ // Re-enable submit button
174
+ if (submitBtn) {
175
+ submitBtn.disabled = false;
176
+ submitBtn.textContent = submitBtn.dataset.originalText || "Subscribe";
177
+ }
178
+ }
179
+ });
180
+ });
181
+ </script>
182
+
183
+ <style is:global>
184
+ @layer seriph {
185
+ .seriph-subscribe {
186
+ --seriph-border-color: #d1d5db;
187
+ --seriph-input-bg: #ffffff;
188
+ --seriph-input-text: #111827;
189
+ --seriph-button-bg: #4f46e5;
190
+ --seriph-button-text: #ffffff;
191
+ --seriph-button-hover: #4338ca;
192
+ --seriph-success-bg: #dcfce7;
193
+ --seriph-success-text: #166534;
194
+ --seriph-success-border: #bbf7d0;
195
+ --seriph-error-bg: #fef2f2;
196
+ --seriph-error-text: #991b1b;
197
+ --seriph-error-border: #fecaca;
198
+ --seriph-focus-ring: rgba(79, 70, 229, 0.5);
199
+
200
+ position: relative;
201
+ }
202
+
203
+ /* Dark theme */
204
+ .seriph-subscribe.seriph-theme-dark {
205
+ --seriph-border-color: #4b5563;
206
+ --seriph-input-bg: #1f2937;
207
+ --seriph-input-text: #f9fafb;
208
+ --seriph-button-bg: #6366f1;
209
+ --seriph-button-hover: #818cf8;
210
+ --seriph-success-bg: rgba(34, 197, 94, 0.15);
211
+ --seriph-success-text: #86efac;
212
+ --seriph-success-border: rgba(34, 197, 94, 0.3);
213
+ --seriph-error-bg: rgba(239, 68, 68, 0.15);
214
+ --seriph-error-text: #fca5a5;
215
+ --seriph-error-border: rgba(239, 68, 68, 0.3);
216
+ }
217
+
218
+ /* Auto theme - follows prefers-color-scheme */
219
+ @media (prefers-color-scheme: dark) {
220
+ .seriph-subscribe.seriph-theme-auto {
221
+ --seriph-border-color: #4b5563;
222
+ --seriph-input-bg: #1f2937;
223
+ --seriph-input-text: #f9fafb;
224
+ --seriph-button-bg: #6366f1;
225
+ --seriph-button-hover: #818cf8;
226
+ --seriph-success-bg: rgba(34, 197, 94, 0.15);
227
+ --seriph-success-text: #86efac;
228
+ --seriph-success-border: rgba(34, 197, 94, 0.3);
229
+ --seriph-error-bg: rgba(239, 68, 68, 0.15);
230
+ --seriph-error-text: #fca5a5;
231
+ --seriph-error-border: rgba(239, 68, 68, 0.3);
232
+ }
233
+ }
234
+
235
+ .seriph-subscribe-input-group {
236
+ display: flex;
237
+ gap: 0.5rem;
238
+ }
239
+
240
+ .seriph-subscribe-input {
241
+ flex: 1;
242
+ padding: 0.625rem 0.875rem;
243
+ border: 1px solid var(--seriph-border-color);
244
+ border-radius: 0.375rem;
245
+ background-color: var(--seriph-input-bg);
246
+ color: var(--seriph-input-text);
247
+ font-size: 0.875rem;
248
+ line-height: 1.25rem;
249
+ }
250
+
251
+ .seriph-subscribe-input:focus {
252
+ outline: none;
253
+ border-color: var(--seriph-button-bg);
254
+ box-shadow: 0 0 0 3px var(--seriph-focus-ring);
255
+ }
256
+
257
+ .seriph-subscribe-input::placeholder {
258
+ color: #9ca3af;
259
+ }
260
+
261
+ .seriph-subscribe-button {
262
+ padding: 0.625rem 1rem;
263
+ border: none;
264
+ border-radius: 0.375rem;
265
+ background-color: var(--seriph-button-bg);
266
+ color: var(--seriph-button-text);
267
+ font-size: 0.875rem;
268
+ font-weight: 500;
269
+ cursor: pointer;
270
+ transition: background-color 0.15s ease;
271
+ white-space: nowrap;
272
+ }
273
+
274
+ .seriph-subscribe-button:hover:not(:disabled) {
275
+ background-color: var(--seriph-button-hover);
276
+ }
277
+
278
+ .seriph-subscribe-button:disabled {
279
+ opacity: 0.6;
280
+ cursor: not-allowed;
281
+ }
282
+
283
+ .seriph-subscribe-message {
284
+ margin-top: 0.75rem;
285
+ padding: 0.625rem 0.875rem;
286
+ border-radius: 0.375rem;
287
+ font-size: 0.875rem;
288
+ }
289
+
290
+ .seriph-subscribe-success {
291
+ background-color: var(--seriph-success-bg);
292
+ color: var(--seriph-success-text);
293
+ border: 1px solid var(--seriph-success-border);
294
+ }
295
+
296
+ .seriph-subscribe-error {
297
+ background-color: var(--seriph-error-bg);
298
+ color: var(--seriph-error-text);
299
+ border: 1px solid var(--seriph-error-border);
300
+ }
301
+ }
302
+ </style>
@@ -0,0 +1,142 @@
1
+ ---
2
+ /**
3
+ * Seriph 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.SERIPH_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.SERIPH_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('seriph:success', (e) => {
24
+ * e.target.querySelector('.message').textContent = e.detail.message;
25
+ * });
26
+ * </script>
27
+ *
28
+ * Events:
29
+ * - seriph:loading - Fired when submission starts
30
+ * - seriph:success - Fired on success, detail contains { success, message }
31
+ * - seriph: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 Seriph instance (default: 'https://seriph.xyz') */
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://seriph.xyz";
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={["seriph-subscribe-form", className]}
61
+ data-seriph-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
+ document.querySelectorAll("[data-seriph-subscribe-form]").forEach((form) => {
80
+ form.addEventListener("submit", async (e) => {
81
+ e.preventDefault();
82
+
83
+ const formEl = e.target as HTMLFormElement;
84
+ const endpoint = formEl.dataset.endpoint;
85
+ const siteKey = formEl.dataset.siteKey;
86
+
87
+ if (!endpoint || !siteKey) {
88
+ console.error("Seriph SubscribeForm: missing siteKey or endpoint");
89
+ return;
90
+ }
91
+
92
+ // Collect form data
93
+ const formData = new FormData(formEl);
94
+ const data: Record<string, unknown> = {};
95
+ formData.forEach((value, key) => {
96
+ data[key] = value;
97
+ });
98
+
99
+ // Dispatch loading event
100
+ formEl.dispatchEvent(new CustomEvent("seriph:loading", { bubbles: true }));
101
+
102
+ try {
103
+ const response = await fetch(`${endpoint}/subscribe`, {
104
+ method: "POST",
105
+ headers: {
106
+ "Content-Type": "application/json",
107
+ "X-Seriph-Key": siteKey,
108
+ },
109
+ body: JSON.stringify(data),
110
+ });
111
+
112
+ if (!response.ok) {
113
+ const errorText = await response.text();
114
+ throw new Error(errorText || "Subscription failed");
115
+ }
116
+
117
+ const result: SubscribeResponse = await response.json();
118
+
119
+ // Dispatch success event
120
+ formEl.dispatchEvent(
121
+ new CustomEvent("seriph:success", {
122
+ bubbles: true,
123
+ detail: result,
124
+ }),
125
+ );
126
+
127
+ // Clear email input on success
128
+ const emailInput = formEl.querySelector('input[name="email"]') as HTMLInputElement | null;
129
+ if (emailInput) {
130
+ emailInput.value = "";
131
+ }
132
+ } catch (error) {
133
+ formEl.dispatchEvent(
134
+ new CustomEvent("seriph:error", {
135
+ bubbles: true,
136
+ detail: error,
137
+ }),
138
+ );
139
+ }
140
+ });
141
+ });
142
+ </script>