@seriphxyz/astro 0.1.5 → 0.1.7

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/README.md CHANGED
@@ -152,6 +152,49 @@ import Reactions from "@seriphxyz/astro/Reactions";
152
152
  - `seriph:reaction-added` - Reaction added
153
153
  - `seriph:reaction-removed` - Reaction removed
154
154
 
155
+ ### Subscribe
156
+
157
+ Email subscription form with double opt-in:
158
+
159
+ ```astro
160
+ ---
161
+ import Subscribe from "@seriphxyz/astro/Subscribe";
162
+ ---
163
+
164
+ <Subscribe
165
+ siteKey={import.meta.env.SERIPH_SITE_KEY}
166
+ buttonText="Subscribe"
167
+ placeholder="your@email.com"
168
+ />
169
+ ```
170
+
171
+ **Props:**
172
+ - `siteKey` (required) - Your Seriph site key
173
+ - `endpoint` - Base URL (default: `https://seriph.xyz`)
174
+ - `buttonText` - Submit button text (default: `'Subscribe'`)
175
+ - `placeholder` - Email input placeholder
176
+ - `successMessage` - Custom success message
177
+ - `theme` - `'light'` | `'dark'` | `'auto'` (default: `'light'`)
178
+ - `class` - Additional CSS class
179
+
180
+ **Events:**
181
+ - `seriph:subscribed` - Subscription successful
182
+
183
+ ### SubscribeForm
184
+
185
+ A more flexible subscription form that wraps your own markup:
186
+
187
+ ```astro
188
+ ---
189
+ import SubscribeForm from "@seriphxyz/astro/SubscribeForm";
190
+ ---
191
+
192
+ <SubscribeForm siteKey={import.meta.env.SERIPH_SITE_KEY}>
193
+ <input name="email" type="email" placeholder="Email" required />
194
+ <button type="submit">Join newsletter</button>
195
+ </SubscribeForm>
196
+ ```
197
+
155
198
  ## JavaScript API
156
199
 
157
200
  For advanced use cases, use the JavaScript API directly:
package/dist/index.d.ts CHANGED
@@ -4,5 +4,5 @@
4
4
  * Astro components and content loader for Seriph widgets.
5
5
  * Re-exports all types, API functions, and controllers from @seriphxyz/core.
6
6
  */
7
- export { DEFAULT_ENDPOINT, API_PATH, type SeriphConfig, type Comment, type ReactionCounts, type FormSubmitResponse, type SubscribeResponse, type SeriphPost, buildUrl, getSiteKey, type SubmitFormOptions, submitForm, type FetchCommentsOptions, fetchComments, type PostCommentOptions, postComment, type FetchReactionsOptions, fetchReactions, type AddReactionOptions, addReaction, type RemoveReactionOptions, removeReaction, type SubscribeOptions, subscribe, type FetchPostsOptions, fetchPosts, type FetchPostOptions, fetchPost, type ControllerStatus, type ControllerListener, type SubscribeState, type FormState, type ReactionsState, type CommentsState, SubscribeController, FormController, ReactionsController, CommentsController, } from "@seriphxyz/core";
7
+ export { DEFAULT_ENDPOINT, API_PATH, VISITOR_STORAGE_KEY, type SeriphConfig, type Comment, type ReactionCounts, type FormSubmitResponse, type SubscribeResponse, type SeriphPost, buildUrl, getSiteKey, getVisitorId, setVisitorId, type SubmitFormOptions, submitForm, type FetchCommentsOptions, fetchComments, type PostCommentOptions, postComment, type FetchReactionsOptions, type FetchReactionsResponse, fetchReactions, type AddReactionOptions, addReaction, type RemoveReactionOptions, removeReaction, type SubscribeOptions, subscribe, type JoinWaitlistOptions, type JoinWaitlistResponse, joinWaitlist, type ViewCountsOptions, type ViewCounts, type RecordViewResponse, getViewCounts, recordView, type FetchPostsOptions, fetchPosts, type FetchPostOptions, fetchPost, type ControllerStatus, type ControllerListener, type SubscribeState, type FormState, type ReactionsState, type CommentsState, type WaitlistState, SubscribeController, WaitlistController, FormController, ReactionsController, CommentsController, } from "@seriphxyz/core";
8
8
  export { seriphPostsLoader, type SeriphPostsLoaderOptions, } from "./loader.js";
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@
7
7
  // Re-export everything from core
8
8
  export {
9
9
  // Constants
10
- DEFAULT_ENDPOINT, API_PATH,
10
+ DEFAULT_ENDPOINT, API_PATH, VISITOR_STORAGE_KEY,
11
11
  // Helpers
12
- buildUrl, getSiteKey, submitForm, fetchComments, postComment, fetchReactions, addReaction, removeReaction, subscribe, fetchPosts, fetchPost, SubscribeController, FormController, ReactionsController, CommentsController, } from "@seriphxyz/core";
12
+ buildUrl, getSiteKey, getVisitorId, setVisitorId, submitForm, fetchComments, postComment, fetchReactions, addReaction, removeReaction, subscribe, joinWaitlist, getViewCounts, recordView, fetchPosts, fetchPost, SubscribeController, WaitlistController, FormController, ReactionsController, CommentsController, } from "@seriphxyz/core";
13
13
  // Re-export loader (Astro-specific)
14
14
  export { seriphPostsLoader, } from "./loader.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seriphxyz/astro",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Astro components and content loader for Seriph widgets (forms, comments, reactions, posts)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -41,7 +41,7 @@
41
41
  ],
42
42
  "license": "MIT",
43
43
  "dependencies": {
44
- "@seriphxyz/core": "0.1.2"
44
+ "@seriphxyz/core": "0.1.7"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "astro": "^5.0.0"
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Seriph Comments Component
4
4
  *
5
- * Displays threaded comments with a form to post new comments.
5
+ * Displays threaded comments with a form to post new comments and replies.
6
6
  * Customize with CSS custom properties.
7
7
  *
8
8
  * @example
@@ -47,6 +47,11 @@ const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
47
47
  <div class="seriph-comments-list"></div>
48
48
 
49
49
  <form class="seriph-comments-form">
50
+ <div class="seriph-reply-indicator" style="display: none;">
51
+ <span>Replying to <strong class="seriph-reply-to-name"></strong></span>
52
+ <button type="button" class="seriph-cancel-reply">Cancel</button>
53
+ </div>
54
+ <input type="hidden" name="parentId" value="" />
50
55
  <slot name="form">
51
56
  <div class="seriph-form-group">
52
57
  <label for="seriph-author-name">Name</label>
@@ -76,6 +81,7 @@ const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
76
81
  <span class="seriph-comment-date"></span>
77
82
  </div>
78
83
  <div class="seriph-comment-content"></div>
84
+ <button type="button" class="seriph-reply-btn">Reply</button>
79
85
  <div class="seriph-comment-replies"></div>
80
86
  </div>
81
87
  </template>
@@ -98,14 +104,28 @@ const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
98
104
  const siteKey = (container as HTMLElement).dataset.siteKey;
99
105
  const pageId = (container as HTMLElement).dataset.pageId;
100
106
  const list = container.querySelector(".seriph-comments-list");
101
- const form = container.querySelector(".seriph-comments-form");
107
+ const form = container.querySelector(".seriph-comments-form") as HTMLFormElement;
102
108
  const template = container.querySelector("#seriph-comment-template") as HTMLTemplateElement;
109
+ const replyIndicator = container.querySelector(".seriph-reply-indicator") as HTMLElement;
110
+ const replyToName = container.querySelector(".seriph-reply-to-name") as HTMLElement;
111
+ const cancelReplyBtn = container.querySelector(".seriph-cancel-reply") as HTMLButtonElement;
112
+ const parentIdInput = form?.querySelector('input[name="parentId"]') as HTMLInputElement;
103
113
 
104
114
  if (!endpoint || !siteKey || !pageId || !list || !form || !template) return;
105
115
 
106
116
  // Record when form was loaded (for time-based spam detection)
107
117
  formTimestamps.set(form, Math.floor(Date.now() / 1000));
108
118
 
119
+ // Cancel reply mode
120
+ function cancelReply() {
121
+ if (parentIdInput) parentIdInput.value = "";
122
+ if (replyIndicator) replyIndicator.style.display = "none";
123
+ const submitBtn = form.querySelector('[type="submit"]') as HTMLButtonElement;
124
+ if (submitBtn) submitBtn.textContent = "Post Comment";
125
+ }
126
+
127
+ cancelReplyBtn?.addEventListener("click", cancelReply);
128
+
109
129
  async function loadComments() {
110
130
  try {
111
131
  const response = await fetch(
@@ -144,6 +164,20 @@ const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
144
164
  ).toLocaleDateString();
145
165
  el.querySelector(".seriph-comment-content")!.textContent = comment.content;
146
166
 
167
+ // Add reply button handler
168
+ const replyBtn = el.querySelector(".seriph-reply-btn") as HTMLButtonElement;
169
+ replyBtn?.addEventListener("click", () => {
170
+ if (parentIdInput) parentIdInput.value = comment.id;
171
+ if (replyToName) replyToName.textContent = comment.authorName;
172
+ if (replyIndicator) replyIndicator.style.display = "flex";
173
+ const submitBtn = form.querySelector('[type="submit"]') as HTMLButtonElement;
174
+ if (submitBtn) submitBtn.textContent = "Post Reply";
175
+ // Scroll form into view and focus
176
+ form.scrollIntoView({ behavior: "smooth", block: "center" });
177
+ const contentField = form.querySelector('textarea[name="content"]') as HTMLTextAreaElement;
178
+ setTimeout(() => contentField?.focus(), 300);
179
+ });
180
+
147
181
  const repliesContainer = el.querySelector(".seriph-comment-replies")!;
148
182
  comment.replies?.forEach((reply) => renderComment(reply, repliesContainer));
149
183
 
@@ -164,6 +198,9 @@ const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
164
198
  // Get load timestamp for time-based spam detection
165
199
  const loadTimestamp = formTimestamps.get(formEl);
166
200
 
201
+ // Get parent ID if replying
202
+ const parentId = formData.get("parentId") as string | null;
203
+
167
204
  try {
168
205
  const response = await fetch(
169
206
  `${endpoint}/comments/${encodeURIComponent(pageId)}`,
@@ -177,6 +214,7 @@ const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
177
214
  authorName: formData.get("authorName"),
178
215
  authorEmail: formData.get("authorEmail") || undefined,
179
216
  content: formData.get("content"),
217
+ parentId: parentId ? parseInt(parentId, 10) : undefined,
180
218
  _gotcha: honeypot || undefined,
181
219
  _seriph_ts: loadTimestamp,
182
220
  }),
@@ -186,6 +224,7 @@ const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
186
224
  if (!response.ok) throw new Error("Failed to post comment");
187
225
 
188
226
  formEl.reset();
227
+ cancelReply(); // Reset reply mode
189
228
  container.dispatchEvent(
190
229
  new CustomEvent("seriph:comment-posted", {
191
230
  detail: await response.json(),
@@ -278,12 +317,40 @@ const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
278
317
  grid-template-columns: 1fr 1fr;
279
318
  }
280
319
 
281
- .seriph-comments-form .seriph-form-group:nth-child(3),
320
+ .seriph-comments-form .seriph-form-group:nth-child(4),
321
+ .seriph-comments-form .seriph-reply-indicator,
282
322
  .seriph-comments-form button[type="submit"] {
283
323
  grid-column: 1 / -1;
284
324
  }
285
325
  }
286
326
 
327
+ .seriph-reply-indicator {
328
+ display: flex;
329
+ align-items: center;
330
+ gap: 0.5rem;
331
+ padding: 0.5rem 0.75rem;
332
+ background: var(--seriph-notice-bg);
333
+ color: var(--seriph-notice-text);
334
+ border-radius: 0.25rem;
335
+ font-size: 0.8125rem;
336
+ grid-column: 1 / -1;
337
+ }
338
+
339
+ .seriph-cancel-reply {
340
+ margin-left: auto;
341
+ padding: 0.125rem 0.5rem;
342
+ background: transparent;
343
+ border: 1px solid currentColor;
344
+ border-radius: 0.25rem;
345
+ color: inherit;
346
+ font-size: 0.75rem;
347
+ cursor: pointer;
348
+ }
349
+
350
+ .seriph-cancel-reply:hover {
351
+ background: rgba(0, 0, 0, 0.1);
352
+ }
353
+
287
354
  .seriph-form-group {
288
355
  margin-bottom: 0;
289
356
  }
@@ -368,6 +435,21 @@ const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
368
435
  font-size: 0.875rem;
369
436
  }
370
437
 
438
+ .seriph-reply-btn {
439
+ margin-top: 0.5rem;
440
+ padding: 0.125rem 0.5rem;
441
+ background: transparent;
442
+ border: none;
443
+ color: var(--seriph-text-muted);
444
+ font-size: 0.75rem;
445
+ cursor: pointer;
446
+ }
447
+
448
+ .seriph-reply-btn:hover {
449
+ color: var(--seriph-focus-color);
450
+ text-decoration: underline;
451
+ }
452
+
371
453
  .seriph-comment-replies {
372
454
  margin-left: 1rem;
373
455
  margin-top: 0.5rem;
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export {
10
10
  // Constants
11
11
  DEFAULT_ENDPOINT,
12
12
  API_PATH,
13
+ VISITOR_STORAGE_KEY,
13
14
 
14
15
  // Types
15
16
  type SeriphConfig,
@@ -22,6 +23,8 @@ export {
22
23
  // Helpers
23
24
  buildUrl,
24
25
  getSiteKey,
26
+ getVisitorId,
27
+ setVisitorId,
25
28
 
26
29
  // API Functions - Forms
27
30
  type SubmitFormOptions,
@@ -35,6 +38,7 @@ export {
35
38
 
36
39
  // API Functions - Reactions
37
40
  type FetchReactionsOptions,
41
+ type FetchReactionsResponse,
38
42
  fetchReactions,
39
43
  type AddReactionOptions,
40
44
  addReaction,
@@ -45,6 +49,18 @@ export {
45
49
  type SubscribeOptions,
46
50
  subscribe,
47
51
 
52
+ // API Functions - Waitlist
53
+ type JoinWaitlistOptions,
54
+ type JoinWaitlistResponse,
55
+ joinWaitlist,
56
+
57
+ // API Functions - Views
58
+ type ViewCountsOptions,
59
+ type ViewCounts,
60
+ type RecordViewResponse,
61
+ getViewCounts,
62
+ recordView,
63
+
48
64
  // API Functions - Posts
49
65
  type FetchPostsOptions,
50
66
  fetchPosts,
@@ -58,7 +74,9 @@ export {
58
74
  type FormState,
59
75
  type ReactionsState,
60
76
  type CommentsState,
77
+ type WaitlistState,
61
78
  SubscribeController,
79
+ WaitlistController,
62
80
  FormController,
63
81
  ReactionsController,
64
82
  CommentsController,