@seriphxyz/astro 0.1.2 → 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 +43 -0
- package/dist/index.d.ts +8 -63
- package/dist/index.js +14 -111
- package/dist/loader.d.ts +5 -53
- package/dist/loader.js +5 -75
- package/package.json +15 -9
- package/src/Comments.astro +85 -3
- package/src/Subscribe.astro +302 -0
- package/src/SubscribeForm.astro +142 -0
- package/src/index.ts +83 -206
- package/src/loader.ts +19 -150
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
|
@@ -1,63 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
/** @deprecated Use SeriphConfig instead */
|
|
10
|
-
export type SeraphConfig = SeriphConfig;
|
|
11
|
-
export { seriphPostsLoader, seraphPostsLoader, fetchPosts, fetchPost, type SeriphPost, type SeraphPost, type SeriphPostsLoaderOptions, type SeraphPostsLoaderOptions, type FetchPostsOptions, type FetchPostOptions, } from "./loader.js";
|
|
12
|
-
export interface FormSubmitOptions {
|
|
13
|
-
onSuccess?: (data: unknown) => void;
|
|
14
|
-
onError?: (error: Error) => void;
|
|
15
|
-
}
|
|
16
|
-
export interface Comment {
|
|
17
|
-
id: string;
|
|
18
|
-
pageId: string;
|
|
19
|
-
parentId?: string;
|
|
20
|
-
authorName: string;
|
|
21
|
-
content: string;
|
|
22
|
-
createdAt: string;
|
|
23
|
-
replies: Comment[];
|
|
24
|
-
}
|
|
25
|
-
export interface ReactionCounts {
|
|
26
|
-
pageId: string;
|
|
27
|
-
counts: Record<string, number>;
|
|
28
|
-
}
|
|
29
|
-
export interface SubmitFormOptions extends SeriphConfig {
|
|
30
|
-
formSlug: string;
|
|
31
|
-
data: Record<string, unknown>;
|
|
32
|
-
/** Form load timestamp for spam detection (auto-set if not provided) */
|
|
33
|
-
formLoadTime?: number;
|
|
34
|
-
}
|
|
35
|
-
export interface FormSubmitResponse {
|
|
36
|
-
success: boolean;
|
|
37
|
-
message: string;
|
|
38
|
-
}
|
|
39
|
-
export declare function submitForm(options: SubmitFormOptions): Promise<FormSubmitResponse>;
|
|
40
|
-
export interface FetchCommentsOptions extends SeriphConfig {
|
|
41
|
-
pageId: string;
|
|
42
|
-
}
|
|
43
|
-
export declare function fetchComments(options: FetchCommentsOptions): Promise<Comment[]>;
|
|
44
|
-
export interface PostCommentOptions extends SeriphConfig {
|
|
45
|
-
pageId: string;
|
|
46
|
-
authorName: string;
|
|
47
|
-
authorEmail?: string;
|
|
48
|
-
content: string;
|
|
49
|
-
parentId?: string;
|
|
50
|
-
}
|
|
51
|
-
export declare function postComment(options: PostCommentOptions): Promise<Comment>;
|
|
52
|
-
export interface FetchReactionsOptions extends SeriphConfig {
|
|
53
|
-
pageId: string;
|
|
54
|
-
}
|
|
55
|
-
export declare function fetchReactions(options: FetchReactionsOptions): Promise<ReactionCounts>;
|
|
56
|
-
export interface AddReactionOptions extends SeriphConfig {
|
|
57
|
-
pageId: string;
|
|
58
|
-
reactionType?: string;
|
|
59
|
-
}
|
|
60
|
-
export declare function addReaction(options: AddReactionOptions): Promise<{
|
|
61
|
-
reactionType: string;
|
|
62
|
-
count: number;
|
|
63
|
-
}>;
|
|
1
|
+
/**
|
|
2
|
+
* @seriphxyz/astro
|
|
3
|
+
*
|
|
4
|
+
* Astro components and content loader for Seriph widgets.
|
|
5
|
+
* Re-exports all types, API functions, and controllers from @seriphxyz/core.
|
|
6
|
+
*/
|
|
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
|
+
export { seriphPostsLoader, type SeriphPostsLoaderOptions, } from "./loader.js";
|
package/dist/index.js
CHANGED
|
@@ -1,111 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
return key;
|
|
17
|
-
}
|
|
18
|
-
// Client utilities
|
|
19
|
-
export async function submitForm(options) {
|
|
20
|
-
const { endpoint, formSlug, data, formLoadTime } = options;
|
|
21
|
-
const siteKey = getSiteKey(options);
|
|
22
|
-
const url = buildUrl(endpoint, `/forms/${formSlug}/submit`);
|
|
23
|
-
// Include timestamp for time-based spam detection
|
|
24
|
-
const payload = {
|
|
25
|
-
...data,
|
|
26
|
-
_seriph_ts: formLoadTime || Math.floor(Date.now() / 1000),
|
|
27
|
-
};
|
|
28
|
-
const response = await fetch(url, {
|
|
29
|
-
method: "POST",
|
|
30
|
-
headers: {
|
|
31
|
-
"Content-Type": "application/json",
|
|
32
|
-
"X-Seriph-Key": siteKey,
|
|
33
|
-
},
|
|
34
|
-
body: JSON.stringify(payload),
|
|
35
|
-
});
|
|
36
|
-
if (!response.ok) {
|
|
37
|
-
throw new Error(`Form submission failed: ${response.statusText}`);
|
|
38
|
-
}
|
|
39
|
-
return response.json();
|
|
40
|
-
}
|
|
41
|
-
export async function fetchComments(options) {
|
|
42
|
-
const { endpoint, pageId } = options;
|
|
43
|
-
const siteKey = getSiteKey(options);
|
|
44
|
-
const url = buildUrl(endpoint, `/comments/${encodeURIComponent(pageId)}`);
|
|
45
|
-
const response = await fetch(url, {
|
|
46
|
-
headers: {
|
|
47
|
-
"X-Seriph-Key": siteKey,
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
if (!response.ok) {
|
|
51
|
-
throw new Error(`Failed to fetch comments: ${response.statusText}`);
|
|
52
|
-
}
|
|
53
|
-
const data = await response.json();
|
|
54
|
-
return data.data || [];
|
|
55
|
-
}
|
|
56
|
-
export async function postComment(options) {
|
|
57
|
-
const { endpoint, pageId, ...rest } = options;
|
|
58
|
-
const siteKey = getSiteKey(options);
|
|
59
|
-
const url = buildUrl(endpoint, `/comments/${encodeURIComponent(pageId)}`);
|
|
60
|
-
const response = await fetch(url, {
|
|
61
|
-
method: "POST",
|
|
62
|
-
headers: {
|
|
63
|
-
"Content-Type": "application/json",
|
|
64
|
-
"X-Seriph-Key": siteKey,
|
|
65
|
-
},
|
|
66
|
-
body: JSON.stringify({
|
|
67
|
-
authorName: rest.authorName,
|
|
68
|
-
authorEmail: rest.authorEmail,
|
|
69
|
-
content: rest.content,
|
|
70
|
-
parentId: rest.parentId,
|
|
71
|
-
}),
|
|
72
|
-
});
|
|
73
|
-
if (!response.ok) {
|
|
74
|
-
throw new Error(`Failed to post comment: ${response.statusText}`);
|
|
75
|
-
}
|
|
76
|
-
const data = await response.json();
|
|
77
|
-
return data.data;
|
|
78
|
-
}
|
|
79
|
-
export async function fetchReactions(options) {
|
|
80
|
-
const { endpoint, pageId } = options;
|
|
81
|
-
const siteKey = getSiteKey(options);
|
|
82
|
-
const url = buildUrl(endpoint, `/reactions/${encodeURIComponent(pageId)}`);
|
|
83
|
-
const response = await fetch(url, {
|
|
84
|
-
headers: {
|
|
85
|
-
"X-Seriph-Key": siteKey,
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
if (!response.ok) {
|
|
89
|
-
throw new Error(`Failed to fetch reactions: ${response.statusText}`);
|
|
90
|
-
}
|
|
91
|
-
const data = await response.json();
|
|
92
|
-
return data.data;
|
|
93
|
-
}
|
|
94
|
-
export async function addReaction(options) {
|
|
95
|
-
const { endpoint, pageId, reactionType = "like" } = options;
|
|
96
|
-
const siteKey = getSiteKey(options);
|
|
97
|
-
const url = buildUrl(endpoint, `/reactions/${encodeURIComponent(pageId)}`);
|
|
98
|
-
const response = await fetch(url, {
|
|
99
|
-
method: "POST",
|
|
100
|
-
headers: {
|
|
101
|
-
"Content-Type": "application/json",
|
|
102
|
-
"X-Seriph-Key": siteKey,
|
|
103
|
-
},
|
|
104
|
-
body: JSON.stringify({ reactionType }),
|
|
105
|
-
});
|
|
106
|
-
if (!response.ok) {
|
|
107
|
-
throw new Error(`Failed to add reaction: ${response.statusText}`);
|
|
108
|
-
}
|
|
109
|
-
const data = await response.json();
|
|
110
|
-
return data.data;
|
|
111
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @seriphxyz/astro
|
|
3
|
+
*
|
|
4
|
+
* Astro components and content loader for Seriph widgets.
|
|
5
|
+
* Re-exports all types, API functions, and controllers from @seriphxyz/core.
|
|
6
|
+
*/
|
|
7
|
+
// Re-export everything from core
|
|
8
|
+
export {
|
|
9
|
+
// Constants
|
|
10
|
+
DEFAULT_ENDPOINT, API_PATH, VISITOR_STORAGE_KEY,
|
|
11
|
+
// Helpers
|
|
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
|
+
// Re-export loader (Astro-specific)
|
|
14
|
+
export { seriphPostsLoader, } from "./loader.js";
|
package/dist/loader.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @example
|
|
7
7
|
* // In src/content.config.ts
|
|
8
8
|
* import { defineCollection } from 'astro:content';
|
|
9
|
-
* import { seriphPostsLoader } from '
|
|
9
|
+
* import { seriphPostsLoader } from '@seriphxyz/astro/loader';
|
|
10
10
|
*
|
|
11
11
|
* const posts = defineCollection({
|
|
12
12
|
* loader: seriphPostsLoader({
|
|
@@ -16,25 +16,12 @@
|
|
|
16
16
|
*
|
|
17
17
|
* export const collections = { posts };
|
|
18
18
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
slug: string;
|
|
23
|
-
content: string;
|
|
24
|
-
excerpt?: string;
|
|
25
|
-
coverImage?: string;
|
|
26
|
-
metaTitle?: string;
|
|
27
|
-
metaDescription?: string;
|
|
28
|
-
tags: string[];
|
|
29
|
-
publishedAt: string;
|
|
30
|
-
}
|
|
31
|
-
/** @deprecated Use SeriphPost instead */
|
|
32
|
-
export type SeraphPost = SeriphPost;
|
|
19
|
+
import { fetchPosts as coreFetchPosts, fetchPost as coreFetchPost, type SeriphPost, type FetchPostsOptions, type FetchPostOptions } from "@seriphxyz/core";
|
|
20
|
+
export type { SeriphPost, FetchPostsOptions, FetchPostOptions };
|
|
21
|
+
export { coreFetchPosts as fetchPosts, coreFetchPost as fetchPost };
|
|
33
22
|
export interface SeriphPostsLoaderOptions {
|
|
34
23
|
/** Your site key (required) */
|
|
35
|
-
siteKey
|
|
36
|
-
/** @deprecated Use siteKey instead */
|
|
37
|
-
apiKey?: string;
|
|
24
|
+
siteKey: string;
|
|
38
25
|
/** Base URL of your Seriph instance (default: 'https://seriph.xyz') */
|
|
39
26
|
endpoint?: string;
|
|
40
27
|
/** Filter posts by tag */
|
|
@@ -44,8 +31,6 @@ export interface SeriphPostsLoaderOptions {
|
|
|
44
31
|
/** How to handle errors: 'throw' (default), 'warn', or 'ignore' */
|
|
45
32
|
onError?: "throw" | "warn" | "ignore";
|
|
46
33
|
}
|
|
47
|
-
/** @deprecated Use SeriphPostsLoaderOptions instead */
|
|
48
|
-
export type SeraphPostsLoaderOptions = SeriphPostsLoaderOptions;
|
|
49
34
|
interface LoaderContext {
|
|
50
35
|
store: {
|
|
51
36
|
set: (entry: {
|
|
@@ -70,36 +55,3 @@ export declare function seriphPostsLoader(options: SeriphPostsLoaderOptions): {
|
|
|
70
55
|
name: string;
|
|
71
56
|
load(context: LoaderContext): Promise<void>;
|
|
72
57
|
};
|
|
73
|
-
/** @deprecated Use seriphPostsLoader instead (note the 'i' in seriph) */
|
|
74
|
-
export declare const seraphPostsLoader: typeof seriphPostsLoader;
|
|
75
|
-
export interface FetchPostsOptions {
|
|
76
|
-
/** Your site key (required) */
|
|
77
|
-
siteKey?: string;
|
|
78
|
-
/** @deprecated Use siteKey instead */
|
|
79
|
-
apiKey?: string;
|
|
80
|
-
/** Base URL of your Seriph instance (default: 'https://seriph.xyz') */
|
|
81
|
-
endpoint?: string;
|
|
82
|
-
/** Filter posts by tag */
|
|
83
|
-
tag?: string;
|
|
84
|
-
/** Maximum number of posts to fetch (default: 500) */
|
|
85
|
-
limit?: number;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Utility function to fetch posts directly (for server-side use cases)
|
|
89
|
-
*/
|
|
90
|
-
export declare function fetchPosts(options: FetchPostsOptions): Promise<SeriphPost[]>;
|
|
91
|
-
export interface FetchPostOptions {
|
|
92
|
-
/** Your site key (required) */
|
|
93
|
-
siteKey?: string;
|
|
94
|
-
/** @deprecated Use siteKey instead */
|
|
95
|
-
apiKey?: string;
|
|
96
|
-
/** Base URL of your Seriph instance (default: 'https://seriph.xyz') */
|
|
97
|
-
endpoint?: string;
|
|
98
|
-
/** The post slug to fetch */
|
|
99
|
-
slug: string;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Utility function to fetch a single post by slug
|
|
103
|
-
*/
|
|
104
|
-
export declare function fetchPost(options: FetchPostOptions): Promise<SeriphPost | null>;
|
|
105
|
-
export {};
|
package/dist/loader.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @example
|
|
7
7
|
* // In src/content.config.ts
|
|
8
8
|
* import { defineCollection } from 'astro:content';
|
|
9
|
-
* import { seriphPostsLoader } from '
|
|
9
|
+
* import { seriphPostsLoader } from '@seriphxyz/astro/loader';
|
|
10
10
|
*
|
|
11
11
|
* const posts = defineCollection({
|
|
12
12
|
* loader: seriphPostsLoader({
|
|
@@ -16,16 +16,8 @@
|
|
|
16
16
|
*
|
|
17
17
|
* export const collections = { posts };
|
|
18
18
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// Helper to get site key (supports both siteKey and deprecated apiKey)
|
|
22
|
-
function getSiteKey(options) {
|
|
23
|
-
const key = options.siteKey || options.apiKey;
|
|
24
|
-
if (!key) {
|
|
25
|
-
throw new Error("siteKey is required");
|
|
26
|
-
}
|
|
27
|
-
return key;
|
|
28
|
-
}
|
|
19
|
+
import { DEFAULT_ENDPOINT, API_PATH, getSiteKey, fetchPosts as coreFetchPosts, fetchPost as coreFetchPost, } from "@seriphxyz/core";
|
|
20
|
+
export { coreFetchPosts as fetchPosts, coreFetchPost as fetchPost };
|
|
29
21
|
/**
|
|
30
22
|
* Creates an Astro content loader that fetches posts from Seriph.
|
|
31
23
|
*
|
|
@@ -33,15 +25,13 @@ function getSiteKey(options) {
|
|
|
33
25
|
*/
|
|
34
26
|
export function seriphPostsLoader(options) {
|
|
35
27
|
const { endpoint = DEFAULT_ENDPOINT, tag, limit = 500, onError = "throw", } = options;
|
|
36
|
-
const siteKey = getSiteKey(options);
|
|
37
|
-
// Build the base API URL (strip trailing slash, add API path)
|
|
28
|
+
const siteKey = getSiteKey({ siteKey: options.siteKey });
|
|
38
29
|
const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
|
|
39
30
|
return {
|
|
40
31
|
name: "seriph-posts-loader",
|
|
41
32
|
async load(context) {
|
|
42
33
|
const { store, logger } = context;
|
|
43
34
|
try {
|
|
44
|
-
// Build the URL with query parameters
|
|
45
35
|
const url = new URL(`${baseUrl}/posts`);
|
|
46
36
|
url.searchParams.set("limit", String(limit));
|
|
47
37
|
if (tag) {
|
|
@@ -57,24 +47,11 @@ export function seriphPostsLoader(options) {
|
|
|
57
47
|
throw new Error(`Failed to fetch posts: ${response.status} ${response.statusText}`);
|
|
58
48
|
}
|
|
59
49
|
const data = await response.json();
|
|
60
|
-
// Clear previous entries
|
|
61
50
|
store.clear();
|
|
62
|
-
// Add each post as an entry
|
|
63
51
|
for (const post of data.posts) {
|
|
64
52
|
store.set({
|
|
65
53
|
id: post.slug,
|
|
66
|
-
data:
|
|
67
|
-
id: post.id,
|
|
68
|
-
title: post.title,
|
|
69
|
-
slug: post.slug,
|
|
70
|
-
content: post.content,
|
|
71
|
-
excerpt: post.excerpt,
|
|
72
|
-
coverImage: post.coverImage,
|
|
73
|
-
metaTitle: post.metaTitle,
|
|
74
|
-
metaDescription: post.metaDescription,
|
|
75
|
-
tags: post.tags,
|
|
76
|
-
publishedAt: post.publishedAt,
|
|
77
|
-
},
|
|
54
|
+
data: post,
|
|
78
55
|
});
|
|
79
56
|
}
|
|
80
57
|
logger.info(`Loaded ${data.posts.length} posts from Seriph`);
|
|
@@ -88,54 +65,7 @@ export function seriphPostsLoader(options) {
|
|
|
88
65
|
else if (onError === "warn") {
|
|
89
66
|
logger.warn(`Error loading posts (continuing anyway): ${message}`);
|
|
90
67
|
}
|
|
91
|
-
// onError === "ignore" - silently continue
|
|
92
68
|
}
|
|
93
69
|
},
|
|
94
70
|
};
|
|
95
71
|
}
|
|
96
|
-
/** @deprecated Use seriphPostsLoader instead (note the 'i' in seriph) */
|
|
97
|
-
export const seraphPostsLoader = seriphPostsLoader;
|
|
98
|
-
/**
|
|
99
|
-
* Utility function to fetch posts directly (for server-side use cases)
|
|
100
|
-
*/
|
|
101
|
-
export async function fetchPosts(options) {
|
|
102
|
-
const { endpoint = DEFAULT_ENDPOINT, tag, limit = 500 } = options;
|
|
103
|
-
const siteKey = getSiteKey(options);
|
|
104
|
-
const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
|
|
105
|
-
const url = new URL(`${baseUrl}/posts`);
|
|
106
|
-
url.searchParams.set("limit", String(limit));
|
|
107
|
-
if (tag) {
|
|
108
|
-
url.searchParams.set("tag", tag);
|
|
109
|
-
}
|
|
110
|
-
const response = await fetch(url.toString(), {
|
|
111
|
-
headers: {
|
|
112
|
-
"X-Seriph-Key": siteKey,
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
if (!response.ok) {
|
|
116
|
-
throw new Error(`Failed to fetch posts: ${response.status} ${response.statusText}`);
|
|
117
|
-
}
|
|
118
|
-
const data = await response.json();
|
|
119
|
-
return data.posts;
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Utility function to fetch a single post by slug
|
|
123
|
-
*/
|
|
124
|
-
export async function fetchPost(options) {
|
|
125
|
-
const { endpoint = DEFAULT_ENDPOINT, slug } = options;
|
|
126
|
-
const siteKey = getSiteKey(options);
|
|
127
|
-
const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
|
|
128
|
-
const response = await fetch(`${baseUrl}/posts/${encodeURIComponent(slug)}`, {
|
|
129
|
-
headers: {
|
|
130
|
-
"X-Seriph-Key": siteKey,
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
if (response.status === 404) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
if (!response.ok) {
|
|
137
|
-
throw new Error(`Failed to fetch post: ${response.status} ${response.statusText}`);
|
|
138
|
-
}
|
|
139
|
-
const data = await response.json();
|
|
140
|
-
return data.public_post || data;
|
|
141
|
-
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seriphxyz/astro",
|
|
3
|
-
"version": "0.1.
|
|
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",
|
|
7
|
-
"url": "https://github.com/seriphxyz/astro"
|
|
7
|
+
"url": "git+https://github.com/seriphxyz/astro.git"
|
|
8
8
|
},
|
|
9
9
|
"homepage": "https://seriph.xyz",
|
|
10
10
|
"author": "Tim Shedor",
|
|
@@ -20,16 +20,14 @@
|
|
|
20
20
|
},
|
|
21
21
|
"./Form": "./src/Form.astro",
|
|
22
22
|
"./Comments": "./src/Comments.astro",
|
|
23
|
-
"./Reactions": "./src/Reactions.astro"
|
|
23
|
+
"./Reactions": "./src/Reactions.astro",
|
|
24
|
+
"./Subscribe": "./src/Subscribe.astro",
|
|
25
|
+
"./SubscribeForm": "./src/SubscribeForm.astro"
|
|
24
26
|
},
|
|
25
27
|
"files": [
|
|
26
28
|
"src",
|
|
27
29
|
"dist"
|
|
28
30
|
],
|
|
29
|
-
"scripts": {
|
|
30
|
-
"build": "tsc",
|
|
31
|
-
"dev": "tsc --watch"
|
|
32
|
-
},
|
|
33
31
|
"keywords": [
|
|
34
32
|
"astro",
|
|
35
33
|
"seriph",
|
|
@@ -38,13 +36,21 @@
|
|
|
38
36
|
"reactions",
|
|
39
37
|
"posts",
|
|
40
38
|
"widgets",
|
|
41
|
-
"content-loader"
|
|
39
|
+
"content-loader",
|
|
40
|
+
"subscribe"
|
|
42
41
|
],
|
|
43
42
|
"license": "MIT",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@seriphxyz/core": "0.1.7"
|
|
45
|
+
},
|
|
44
46
|
"peerDependencies": {
|
|
45
47
|
"astro": "^5.0.0"
|
|
46
48
|
},
|
|
47
49
|
"devDependencies": {
|
|
48
50
|
"typescript": "^5.7.3"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsc",
|
|
54
|
+
"dev": "tsc --watch"
|
|
49
55
|
}
|
|
50
|
-
}
|
|
56
|
+
}
|
package/src/Comments.astro
CHANGED
|
@@ -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(
|
|
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;
|