@jamwidgets/react 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/README.md ADDED
@@ -0,0 +1,288 @@
1
+ # @jamwidgets/react
2
+
3
+ > **Note:** This repo is a read-only mirror. Source lives in a private monorepo.
4
+ > For issues/PRs, please open them here and we'll sync changes back.
5
+
6
+ React hooks for [JamWidgets](https://jamwidgets.com) widgets - comments, reactions, forms, subscriptions, and more.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @jamwidgets/react
12
+ ```
13
+
14
+ Works with React 18+ and React 19. Compatible with Next.js, Remix, Vite, and more.
15
+
16
+ ## Hooks
17
+
18
+ ### useSubscribe
19
+
20
+ Email subscription form:
21
+
22
+ ```tsx
23
+ import { useSubscribe } from "@jamwidgets/react";
24
+
25
+ function SubscribeForm() {
26
+ const [email, setEmail] = useState("");
27
+ const { submit, status, message, error } = useSubscribe({
28
+ siteKey: "your-key",
29
+ });
30
+
31
+ const handleSubmit = async (e) => {
32
+ e.preventDefault();
33
+ await submit(email);
34
+ };
35
+
36
+ return (
37
+ <form onSubmit={handleSubmit}>
38
+ <input
39
+ type="email"
40
+ value={email}
41
+ onChange={(e) => setEmail(e.target.value)}
42
+ placeholder="your@email.com"
43
+ />
44
+ <button disabled={status === "loading"}>
45
+ {status === "loading" ? "Subscribing..." : "Subscribe"}
46
+ </button>
47
+ {status === "success" && <p>{message}</p>}
48
+ {status === "error" && <p>{error?.message}</p>}
49
+ </form>
50
+ );
51
+ }
52
+ ```
53
+
54
+ ### useReactions
55
+
56
+ Reaction buttons (like, love, clap, etc.):
57
+
58
+ ```tsx
59
+ import { useReactions } from "@jamwidgets/react";
60
+
61
+ function LikeButton() {
62
+ const { counts, userReactions, add, remove, status } = useReactions({
63
+ siteKey: "your-key",
64
+ pageId: "my-page",
65
+ });
66
+
67
+ const hasLiked = userReactions.includes("like");
68
+
69
+ return (
70
+ <button onClick={() => (hasLiked ? remove("like") : add("like"))}>
71
+ {hasLiked ? "Unlike" : "Like"} ({counts.like || 0})
72
+ </button>
73
+ );
74
+ }
75
+ ```
76
+
77
+ ### useComments
78
+
79
+ Threaded comments:
80
+
81
+ ```tsx
82
+ import { useComments } from "@jamwidgets/react";
83
+
84
+ function Comments() {
85
+ const { comments, post, status, error } = useComments({
86
+ siteKey: "your-key",
87
+ pageId: "my-page",
88
+ });
89
+
90
+ const handleSubmit = async (name, content) => {
91
+ await post(name, content);
92
+ };
93
+
94
+ return (
95
+ <div>
96
+ {comments.map((comment) => (
97
+ <div key={comment.id}>
98
+ <strong>{comment.authorName}</strong>
99
+ <p>{comment.content}</p>
100
+ </div>
101
+ ))}
102
+ </div>
103
+ );
104
+ }
105
+ ```
106
+
107
+ ### useForm
108
+
109
+ Contact forms with spam protection:
110
+
111
+ ```tsx
112
+ import { useForm } from "@jamwidgets/react";
113
+
114
+ function ContactForm() {
115
+ const { submit, status, message } = useForm({
116
+ siteKey: "your-key",
117
+ formSlug: "contact",
118
+ });
119
+
120
+ const handleSubmit = async (e) => {
121
+ e.preventDefault();
122
+ const data = Object.fromEntries(new FormData(e.target));
123
+ await submit(data);
124
+ };
125
+
126
+ return (
127
+ <form onSubmit={handleSubmit}>
128
+ <input name="email" type="email" required />
129
+ <textarea name="message" required />
130
+ <button type="submit">Send</button>
131
+ {status === "success" && <p>{message}</p>}
132
+ </form>
133
+ );
134
+ }
135
+ ```
136
+
137
+ ### useWaitlist
138
+
139
+ Waitlist signups:
140
+
141
+ ```tsx
142
+ import { useWaitlist } from "@jamwidgets/react";
143
+
144
+ function WaitlistForm() {
145
+ const { join, status, message, position } = useWaitlist({
146
+ siteKey: "your-key",
147
+ });
148
+
149
+ const handleSubmit = async (e) => {
150
+ e.preventDefault();
151
+ await join(email, { name, source: "homepage" });
152
+ };
153
+
154
+ return (
155
+ <form onSubmit={handleSubmit}>
156
+ <input name="email" type="email" required />
157
+ <button type="submit">Join Waitlist</button>
158
+ {status === "success" && <p>{message}</p>}
159
+ </form>
160
+ );
161
+ }
162
+ ```
163
+
164
+ ### useFeedback
165
+
166
+ Feedback forms:
167
+
168
+ ```tsx
169
+ import { useFeedback } from "@jamwidgets/react";
170
+
171
+ function FeedbackWidget() {
172
+ const { submit, status, message } = useFeedback({
173
+ siteKey: "your-key",
174
+ });
175
+
176
+ const handleSubmit = async (type, content) => {
177
+ await submit(type, content, { email, pageUrl: window.location.href });
178
+ };
179
+
180
+ // ...
181
+ }
182
+ ```
183
+
184
+ ### usePoll
185
+
186
+ Polls and voting:
187
+
188
+ ```tsx
189
+ import { usePoll } from "@jamwidgets/react";
190
+
191
+ function Poll() {
192
+ const { poll, vote, hasVoted, status } = usePoll({
193
+ siteKey: "your-key",
194
+ pollId: 123,
195
+ });
196
+
197
+ if (!poll) return <div>Loading...</div>;
198
+
199
+ return (
200
+ <div>
201
+ <h3>{poll.question}</h3>
202
+ {poll.options.map((option) => (
203
+ <button
204
+ key={option.id}
205
+ onClick={() => vote([option.id])}
206
+ disabled={hasVoted}
207
+ >
208
+ {option.text} ({poll.results?.[option.id] || 0})
209
+ </button>
210
+ ))}
211
+ </div>
212
+ );
213
+ }
214
+ ```
215
+
216
+ ### useAnnouncements
217
+
218
+ Site announcements:
219
+
220
+ ```tsx
221
+ import { useAnnouncements } from "@jamwidgets/react";
222
+
223
+ function AnnouncementBanner() {
224
+ const { announcements, dismiss, status } = useAnnouncements({
225
+ siteKey: "your-key",
226
+ });
227
+
228
+ const visible = announcements.filter((a) => !a.dismissed);
229
+
230
+ return (
231
+ <>
232
+ {visible.map((announcement) => (
233
+ <div key={announcement.id}>
234
+ <p>{announcement.content}</p>
235
+ {announcement.isDismissible && (
236
+ <button onClick={() => dismiss(announcement.id)}>Dismiss</button>
237
+ )}
238
+ </div>
239
+ ))}
240
+ </>
241
+ );
242
+ }
243
+ ```
244
+
245
+ ### useViewCounts
246
+
247
+ Page view tracking:
248
+
249
+ ```tsx
250
+ import { useViewCounts } from "@jamwidgets/react";
251
+ import { useEffect } from "react";
252
+
253
+ function PageViews() {
254
+ const { views, uniqueVisitors, record, status } = useViewCounts({
255
+ siteKey: "your-key",
256
+ pageId: "my-page",
257
+ });
258
+
259
+ // Record view on mount
260
+ useEffect(() => {
261
+ record();
262
+ }, []);
263
+
264
+ return (
265
+ <span>
266
+ {views} views ({uniqueVisitors} unique)
267
+ </span>
268
+ );
269
+ }
270
+ ```
271
+
272
+ ## All Hooks
273
+
274
+ | Hook | Purpose |
275
+ |------|---------|
276
+ | `useSubscribe` | Email subscriptions |
277
+ | `useReactions` | Page reactions |
278
+ | `useComments` | Threaded comments |
279
+ | `useForm` | Form submissions |
280
+ | `useWaitlist` | Waitlist signups |
281
+ | `useFeedback` | Feedback forms |
282
+ | `usePoll` | Polls and voting |
283
+ | `useAnnouncements` | Site announcements |
284
+ | `useViewCounts` | Page view tracking |
285
+
286
+ ## License
287
+
288
+ MIT
@@ -0,0 +1,310 @@
1
+ /**
2
+ * @jamwidgets/react - React hooks for Jamwidgets
3
+ *
4
+ * @example Subscribe form
5
+ * ```tsx
6
+ * import { useSubscribe } from '@jamwidgets/react';
7
+ *
8
+ * function Newsletter() {
9
+ * const { subscribe, status, error } = useSubscribe({
10
+ * siteKey: 'your-site-key',
11
+ * });
12
+ *
13
+ * const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
14
+ * e.preventDefault();
15
+ * const email = new FormData(e.currentTarget).get('email') as string;
16
+ * subscribe(email);
17
+ * };
18
+ *
19
+ * return (
20
+ * <form onSubmit={handleSubmit}>
21
+ * <input type="email" name="email" required />
22
+ * <button disabled={status === 'loading'}>
23
+ * {status === 'loading' ? 'Subscribing...' : 'Subscribe'}
24
+ * </button>
25
+ * {status === 'success' && <p>Thanks for subscribing!</p>}
26
+ * {status === 'error' && <p>Error: {error?.message}</p>}
27
+ * </form>
28
+ * );
29
+ * }
30
+ * ```
31
+ */
32
+ import { type JamWidgetsConfig, type Comment, type Announcement, type PollWithResults, type FeedbackType, type ControllerStatus } from "@jamwidgets/core";
33
+ export type { JamWidgetsConfig, SubscribeState, FormState, ReactionsState, CommentsState, WaitlistState, ViewCountsState, FeedbackState, PollState, AnnouncementsState, Comment, Announcement, PollWithResults, FeedbackType, ReactionCounts, JamwidgetsPost, SeriphPost, // deprecated alias
34
+ FetchPostsOptions, FetchPostOptions, ControllerStatus, } from "@jamwidgets/core";
35
+ export { fetchPosts, fetchPost, getConfigFromMeta, resolveConfig, DEFAULT_ENDPOINT, API_PATH, } from "@jamwidgets/core";
36
+ type OptionalSiteKey<T extends JamWidgetsConfig> = Omit<T, "siteKey"> & {
37
+ /** Site key - optional if <meta name="jamwidgets-site-key"> is set */
38
+ siteKey?: string;
39
+ };
40
+ export interface UseSubscribeOptions extends OptionalSiteKey<JamWidgetsConfig> {
41
+ }
42
+ export interface UseSubscribeReturn {
43
+ status: ControllerStatus;
44
+ message: string | null;
45
+ error: Error | null;
46
+ subscribe: (email: string) => Promise<void>;
47
+ reset: () => void;
48
+ }
49
+ /**
50
+ * Hook for handling email subscriptions.
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * const { subscribe, status, message, error } = useSubscribe({
55
+ * siteKey: 'your-site-key',
56
+ * });
57
+ *
58
+ * <button onClick={() => subscribe('user@example.com')}>Subscribe</button>
59
+ * ```
60
+ */
61
+ export declare function useSubscribe(options: UseSubscribeOptions): UseSubscribeReturn;
62
+ export interface UseFormOptions extends OptionalSiteKey<JamWidgetsConfig> {
63
+ /** Form slug/identifier */
64
+ formSlug: string;
65
+ }
66
+ export interface UseFormReturn {
67
+ status: ControllerStatus;
68
+ message: string | null;
69
+ error: Error | null;
70
+ submit: (data: Record<string, unknown>) => Promise<void>;
71
+ reset: () => void;
72
+ }
73
+ /**
74
+ * Hook for handling form submissions.
75
+ *
76
+ * @example
77
+ * ```tsx
78
+ * const { submit, status, error } = useForm({
79
+ * siteKey: 'your-site-key',
80
+ * formSlug: 'contact',
81
+ * });
82
+ *
83
+ * const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
84
+ * e.preventDefault();
85
+ * const formData = new FormData(e.currentTarget);
86
+ * submit(Object.fromEntries(formData));
87
+ * };
88
+ * ```
89
+ */
90
+ export declare function useForm(options: UseFormOptions): UseFormReturn;
91
+ export interface UseReactionsOptions extends OptionalSiteKey<JamWidgetsConfig> {
92
+ /** Content identifier (e.g., post slug) */
93
+ contentId: string;
94
+ /** Auto-fetch reactions on mount (default: true) */
95
+ autoFetch?: boolean;
96
+ }
97
+ export interface UseReactionsReturn {
98
+ counts: Record<string, number>;
99
+ userReactions: string[];
100
+ status: ControllerStatus;
101
+ error: Error | null;
102
+ addReaction: (type: string) => Promise<void>;
103
+ removeReaction: (type: string) => Promise<void>;
104
+ refresh: () => Promise<void>;
105
+ }
106
+ /**
107
+ * Hook for handling reactions (likes, claps, etc.).
108
+ *
109
+ * @example
110
+ * ```tsx
111
+ * const { counts, userReactions, addReaction, removeReaction } = useReactions({
112
+ * siteKey: 'your-site-key',
113
+ * contentId: 'my-post-slug',
114
+ * });
115
+ *
116
+ * <button onClick={() => addReaction('like')}>
117
+ * Like ({counts.like || 0})
118
+ * </button>
119
+ * ```
120
+ */
121
+ export declare function useReactions(options: UseReactionsOptions): UseReactionsReturn;
122
+ export interface UseCommentsOptions extends OptionalSiteKey<JamWidgetsConfig> {
123
+ /** Content identifier (e.g., post slug) */
124
+ contentId: string;
125
+ /** Auto-fetch comments on mount (default: true) */
126
+ autoFetch?: boolean;
127
+ }
128
+ export interface UseCommentsReturn {
129
+ comments: Comment[];
130
+ status: ControllerStatus;
131
+ error: Error | null;
132
+ postComment: (author: string, content: string, options?: {
133
+ authorEmail?: string;
134
+ parentId?: string;
135
+ }) => Promise<void>;
136
+ refresh: () => Promise<void>;
137
+ }
138
+ /**
139
+ * Hook for handling comments.
140
+ *
141
+ * @example
142
+ * ```tsx
143
+ * const { comments, status, postComment } = useComments({
144
+ * siteKey: 'your-site-key',
145
+ * contentId: 'my-post-slug',
146
+ * });
147
+ *
148
+ * {comments.map(comment => (
149
+ * <div key={comment.id}>
150
+ * <strong>{comment.authorName}</strong>: {comment.content}
151
+ * </div>
152
+ * ))}
153
+ *
154
+ * <button onClick={() => postComment('Anonymous', 'Great post!')}>
155
+ * Add Comment
156
+ * </button>
157
+ * ```
158
+ */
159
+ export declare function useComments(options: UseCommentsOptions): UseCommentsReturn;
160
+ export interface UseWaitlistOptions extends OptionalSiteKey<JamWidgetsConfig> {
161
+ }
162
+ export interface UseWaitlistReturn {
163
+ status: ControllerStatus;
164
+ message: string | null;
165
+ position: number | null;
166
+ error: Error | null;
167
+ join: (email: string, options?: {
168
+ name?: string;
169
+ source?: string;
170
+ }) => Promise<void>;
171
+ reset: () => void;
172
+ }
173
+ /**
174
+ * Hook for handling waitlist signups.
175
+ *
176
+ * @example
177
+ * ```tsx
178
+ * const { join, status, message, position } = useWaitlist({
179
+ * siteKey: 'your-site-key',
180
+ * });
181
+ *
182
+ * <button onClick={() => join('user@example.com')}>Join Waitlist</button>
183
+ * {status === 'success' && <p>You're #{position} on the list!</p>}
184
+ * ```
185
+ */
186
+ export declare function useWaitlist(options: UseWaitlistOptions): UseWaitlistReturn;
187
+ export interface UseViewsOptions extends OptionalSiteKey<JamWidgetsConfig> {
188
+ /** Page identifier (e.g., slug or URL path) */
189
+ pageId: string;
190
+ /** Auto-record view on mount (default: true) */
191
+ autoRecord?: boolean;
192
+ }
193
+ export interface UseViewsReturn {
194
+ views: number;
195
+ uniqueVisitors: number;
196
+ status: ControllerStatus;
197
+ error: Error | null;
198
+ record: () => Promise<void>;
199
+ refresh: () => Promise<void>;
200
+ }
201
+ /**
202
+ * Hook for tracking and displaying page views.
203
+ *
204
+ * @example
205
+ * ```tsx
206
+ * const { views, uniqueVisitors } = useViews({
207
+ * siteKey: 'your-site-key',
208
+ * pageId: '/blog/my-post',
209
+ * });
210
+ *
211
+ * <span>{views} views ({uniqueVisitors} unique)</span>
212
+ * ```
213
+ */
214
+ export declare function useViews(options: UseViewsOptions): UseViewsReturn;
215
+ export interface UseFeedbackOptions extends OptionalSiteKey<JamWidgetsConfig> {
216
+ }
217
+ export interface UseFeedbackReturn {
218
+ status: ControllerStatus;
219
+ message: string | null;
220
+ error: Error | null;
221
+ submit: (type: FeedbackType, content: string, options?: {
222
+ email?: string;
223
+ pageUrl?: string;
224
+ }) => Promise<void>;
225
+ reset: () => void;
226
+ }
227
+ /**
228
+ * Hook for handling feedback submissions.
229
+ *
230
+ * @example
231
+ * ```tsx
232
+ * const { submit, status } = useFeedback({
233
+ * siteKey: 'your-site-key',
234
+ * });
235
+ *
236
+ * <button onClick={() => submit('feature', 'Add dark mode!')}>
237
+ * Submit Feedback
238
+ * </button>
239
+ * ```
240
+ */
241
+ export declare function useFeedback(options: UseFeedbackOptions): UseFeedbackReturn;
242
+ export interface UsePollOptions extends OptionalSiteKey<JamWidgetsConfig> {
243
+ /** Poll slug */
244
+ slug: string;
245
+ /** Auto-fetch poll on mount (default: true) */
246
+ autoFetch?: boolean;
247
+ }
248
+ export interface UsePollReturn {
249
+ poll: PollWithResults | null;
250
+ status: ControllerStatus;
251
+ error: Error | null;
252
+ vote: (selectedOptions: string[]) => Promise<void>;
253
+ hasVoted: boolean;
254
+ refresh: () => Promise<void>;
255
+ }
256
+ /**
257
+ * Hook for displaying and voting on polls.
258
+ *
259
+ * @example
260
+ * ```tsx
261
+ * const { poll, vote, hasVoted } = usePoll({
262
+ * siteKey: 'your-site-key',
263
+ * slug: 'favorite-framework',
264
+ * });
265
+ *
266
+ * {poll && (
267
+ * <div>
268
+ * <h3>{poll.question}</h3>
269
+ * {poll.options.map(opt => (
270
+ * <button key={opt.id} onClick={() => vote([opt.id])} disabled={hasVoted}>
271
+ * {opt.text} ({poll.results[opt.id] || 0} votes)
272
+ * </button>
273
+ * ))}
274
+ * </div>
275
+ * )}
276
+ * ```
277
+ */
278
+ export declare function usePoll(options: UsePollOptions): UsePollReturn;
279
+ export interface UseAnnouncementsOptions extends OptionalSiteKey<JamWidgetsConfig> {
280
+ /** Auto-fetch announcements on mount (default: true) */
281
+ autoFetch?: boolean;
282
+ }
283
+ export interface UseAnnouncementsReturn {
284
+ announcements: Announcement[];
285
+ status: ControllerStatus;
286
+ error: Error | null;
287
+ dismiss: (announcementId: number) => Promise<void>;
288
+ refresh: () => Promise<void>;
289
+ }
290
+ /**
291
+ * Hook for displaying site announcements.
292
+ *
293
+ * @example
294
+ * ```tsx
295
+ * const { announcements, dismiss } = useAnnouncements({
296
+ * siteKey: 'your-site-key',
297
+ * });
298
+ *
299
+ * {announcements.map(ann => (
300
+ * <div key={ann.id} className={`announcement-${ann.announcementType}`}>
301
+ * {ann.content}
302
+ * {ann.isDismissible && (
303
+ * <button onClick={() => dismiss(ann.id)}>Dismiss</button>
304
+ * )}
305
+ * </div>
306
+ * ))}
307
+ * ```
308
+ */
309
+ export declare function useAnnouncements(options: UseAnnouncementsOptions): UseAnnouncementsReturn;
310
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,410 @@
1
+ /**
2
+ * @jamwidgets/react - React hooks for Jamwidgets
3
+ *
4
+ * @example Subscribe form
5
+ * ```tsx
6
+ * import { useSubscribe } from '@jamwidgets/react';
7
+ *
8
+ * function Newsletter() {
9
+ * const { subscribe, status, error } = useSubscribe({
10
+ * siteKey: 'your-site-key',
11
+ * });
12
+ *
13
+ * const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
14
+ * e.preventDefault();
15
+ * const email = new FormData(e.currentTarget).get('email') as string;
16
+ * subscribe(email);
17
+ * };
18
+ *
19
+ * return (
20
+ * <form onSubmit={handleSubmit}>
21
+ * <input type="email" name="email" required />
22
+ * <button disabled={status === 'loading'}>
23
+ * {status === 'loading' ? 'Subscribing...' : 'Subscribe'}
24
+ * </button>
25
+ * {status === 'success' && <p>Thanks for subscribing!</p>}
26
+ * {status === 'error' && <p>Error: {error?.message}</p>}
27
+ * </form>
28
+ * );
29
+ * }
30
+ * ```
31
+ */
32
+ import { useState, useCallback, useEffect, useRef } from "react";
33
+ import { SubscribeController, FormController, ReactionsController, CommentsController, WaitlistController, ViewCountsController, FeedbackController, PollController, AnnouncementsController, resolveConfig, } from "@jamwidgets/core";
34
+ // Re-export API functions and helpers from core
35
+ export { fetchPosts, fetchPost, getConfigFromMeta, resolveConfig, DEFAULT_ENDPOINT, API_PATH, } from "@jamwidgets/core";
36
+ /**
37
+ * Hook for handling email subscriptions.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * const { subscribe, status, message, error } = useSubscribe({
42
+ * siteKey: 'your-site-key',
43
+ * });
44
+ *
45
+ * <button onClick={() => subscribe('user@example.com')}>Subscribe</button>
46
+ * ```
47
+ */
48
+ export function useSubscribe(options) {
49
+ const controllerRef = useRef(null);
50
+ const [state, setState] = useState({
51
+ status: "idle",
52
+ message: null,
53
+ error: null,
54
+ });
55
+ useEffect(() => {
56
+ const config = resolveConfig(options);
57
+ const controller = new SubscribeController(config);
58
+ controllerRef.current = controller;
59
+ const unsubscribe = controller.subscribe(setState);
60
+ return unsubscribe;
61
+ }, [options.siteKey, options.endpoint]);
62
+ const subscribe = useCallback(async (email) => {
63
+ await controllerRef.current?.submit(email);
64
+ }, []);
65
+ const reset = useCallback(() => {
66
+ controllerRef.current?.reset();
67
+ }, []);
68
+ return { ...state, subscribe, reset };
69
+ }
70
+ /**
71
+ * Hook for handling form submissions.
72
+ *
73
+ * @example
74
+ * ```tsx
75
+ * const { submit, status, error } = useForm({
76
+ * siteKey: 'your-site-key',
77
+ * formSlug: 'contact',
78
+ * });
79
+ *
80
+ * const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
81
+ * e.preventDefault();
82
+ * const formData = new FormData(e.currentTarget);
83
+ * submit(Object.fromEntries(formData));
84
+ * };
85
+ * ```
86
+ */
87
+ export function useForm(options) {
88
+ const controllerRef = useRef(null);
89
+ const [state, setState] = useState({
90
+ status: "idle",
91
+ message: null,
92
+ error: null,
93
+ });
94
+ useEffect(() => {
95
+ const config = resolveConfig(options);
96
+ const controller = new FormController(config, options.formSlug);
97
+ controllerRef.current = controller;
98
+ const unsubscribe = controller.subscribe(setState);
99
+ return unsubscribe;
100
+ }, [options.siteKey, options.endpoint, options.formSlug]);
101
+ const submit = useCallback(async (data) => {
102
+ await controllerRef.current?.submit(data);
103
+ }, []);
104
+ const reset = useCallback(() => {
105
+ controllerRef.current?.reset();
106
+ }, []);
107
+ return { ...state, submit, reset };
108
+ }
109
+ /**
110
+ * Hook for handling reactions (likes, claps, etc.).
111
+ *
112
+ * @example
113
+ * ```tsx
114
+ * const { counts, userReactions, addReaction, removeReaction } = useReactions({
115
+ * siteKey: 'your-site-key',
116
+ * contentId: 'my-post-slug',
117
+ * });
118
+ *
119
+ * <button onClick={() => addReaction('like')}>
120
+ * Like ({counts.like || 0})
121
+ * </button>
122
+ * ```
123
+ */
124
+ export function useReactions(options) {
125
+ const controllerRef = useRef(null);
126
+ const [state, setState] = useState({
127
+ counts: {},
128
+ userReactions: [],
129
+ status: "idle",
130
+ error: null,
131
+ });
132
+ useEffect(() => {
133
+ const config = resolveConfig(options);
134
+ const controller = new ReactionsController(config, options.contentId);
135
+ controllerRef.current = controller;
136
+ const unsubscribe = controller.subscribe(setState);
137
+ // Auto-fetch on mount (default: true)
138
+ if (options.autoFetch !== false) {
139
+ controller.fetch();
140
+ }
141
+ return unsubscribe;
142
+ }, [options.siteKey, options.endpoint, options.contentId]);
143
+ const addReaction = useCallback(async (type) => {
144
+ await controllerRef.current?.add(type);
145
+ }, []);
146
+ const removeReaction = useCallback(async (type) => {
147
+ await controllerRef.current?.remove(type);
148
+ }, []);
149
+ const refresh = useCallback(async () => {
150
+ await controllerRef.current?.fetch();
151
+ }, []);
152
+ return { ...state, addReaction, removeReaction, refresh };
153
+ }
154
+ /**
155
+ * Hook for handling comments.
156
+ *
157
+ * @example
158
+ * ```tsx
159
+ * const { comments, status, postComment } = useComments({
160
+ * siteKey: 'your-site-key',
161
+ * contentId: 'my-post-slug',
162
+ * });
163
+ *
164
+ * {comments.map(comment => (
165
+ * <div key={comment.id}>
166
+ * <strong>{comment.authorName}</strong>: {comment.content}
167
+ * </div>
168
+ * ))}
169
+ *
170
+ * <button onClick={() => postComment('Anonymous', 'Great post!')}>
171
+ * Add Comment
172
+ * </button>
173
+ * ```
174
+ */
175
+ export function useComments(options) {
176
+ const controllerRef = useRef(null);
177
+ const [state, setState] = useState({
178
+ comments: [],
179
+ status: "idle",
180
+ error: null,
181
+ });
182
+ useEffect(() => {
183
+ const config = resolveConfig(options);
184
+ const controller = new CommentsController(config, options.contentId);
185
+ controllerRef.current = controller;
186
+ const unsubscribe = controller.subscribe(setState);
187
+ // Auto-fetch on mount (default: true)
188
+ if (options.autoFetch !== false) {
189
+ controller.fetch();
190
+ }
191
+ return unsubscribe;
192
+ }, [options.siteKey, options.endpoint, options.contentId]);
193
+ const postComment = useCallback(async (author, content, options) => {
194
+ await controllerRef.current?.post(author, content, options);
195
+ }, []);
196
+ const refresh = useCallback(async () => {
197
+ await controllerRef.current?.fetch();
198
+ }, []);
199
+ return { ...state, postComment, refresh };
200
+ }
201
+ /**
202
+ * Hook for handling waitlist signups.
203
+ *
204
+ * @example
205
+ * ```tsx
206
+ * const { join, status, message, position } = useWaitlist({
207
+ * siteKey: 'your-site-key',
208
+ * });
209
+ *
210
+ * <button onClick={() => join('user@example.com')}>Join Waitlist</button>
211
+ * {status === 'success' && <p>You're #{position} on the list!</p>}
212
+ * ```
213
+ */
214
+ export function useWaitlist(options) {
215
+ const controllerRef = useRef(null);
216
+ const [state, setState] = useState({
217
+ status: "idle",
218
+ message: null,
219
+ position: null,
220
+ error: null,
221
+ });
222
+ useEffect(() => {
223
+ const config = resolveConfig(options);
224
+ const controller = new WaitlistController(config);
225
+ controllerRef.current = controller;
226
+ const unsubscribe = controller.subscribe(setState);
227
+ return unsubscribe;
228
+ }, [options.siteKey, options.endpoint]);
229
+ const join = useCallback(async (email, opts) => {
230
+ await controllerRef.current?.join(email, opts);
231
+ }, []);
232
+ const reset = useCallback(() => {
233
+ controllerRef.current?.reset();
234
+ }, []);
235
+ return { ...state, join, reset };
236
+ }
237
+ /**
238
+ * Hook for tracking and displaying page views.
239
+ *
240
+ * @example
241
+ * ```tsx
242
+ * const { views, uniqueVisitors } = useViews({
243
+ * siteKey: 'your-site-key',
244
+ * pageId: '/blog/my-post',
245
+ * });
246
+ *
247
+ * <span>{views} views ({uniqueVisitors} unique)</span>
248
+ * ```
249
+ */
250
+ export function useViews(options) {
251
+ const controllerRef = useRef(null);
252
+ const [state, setState] = useState({
253
+ pageId: options.pageId,
254
+ views: 0,
255
+ uniqueVisitors: 0,
256
+ status: "idle",
257
+ error: null,
258
+ });
259
+ useEffect(() => {
260
+ const config = resolveConfig(options);
261
+ const controller = new ViewCountsController(config, options.pageId);
262
+ controllerRef.current = controller;
263
+ const unsubscribe = controller.subscribe(setState);
264
+ // Auto-record view on mount (default: true)
265
+ if (options.autoRecord !== false) {
266
+ controller.record();
267
+ }
268
+ return unsubscribe;
269
+ }, [options.siteKey, options.endpoint, options.pageId]);
270
+ const record = useCallback(async () => {
271
+ await controllerRef.current?.record();
272
+ }, []);
273
+ const refresh = useCallback(async () => {
274
+ await controllerRef.current?.fetch();
275
+ }, []);
276
+ return { views: state.views, uniqueVisitors: state.uniqueVisitors, status: state.status, error: state.error, record, refresh };
277
+ }
278
+ /**
279
+ * Hook for handling feedback submissions.
280
+ *
281
+ * @example
282
+ * ```tsx
283
+ * const { submit, status } = useFeedback({
284
+ * siteKey: 'your-site-key',
285
+ * });
286
+ *
287
+ * <button onClick={() => submit('feature', 'Add dark mode!')}>
288
+ * Submit Feedback
289
+ * </button>
290
+ * ```
291
+ */
292
+ export function useFeedback(options) {
293
+ const controllerRef = useRef(null);
294
+ const [state, setState] = useState({
295
+ status: "idle",
296
+ message: null,
297
+ error: null,
298
+ });
299
+ useEffect(() => {
300
+ const config = resolveConfig(options);
301
+ const controller = new FeedbackController(config);
302
+ controllerRef.current = controller;
303
+ const unsubscribe = controller.subscribe(setState);
304
+ return unsubscribe;
305
+ }, [options.siteKey, options.endpoint]);
306
+ const submit = useCallback(async (type, content, opts) => {
307
+ await controllerRef.current?.submit(type, content, opts);
308
+ }, []);
309
+ const reset = useCallback(() => {
310
+ controllerRef.current?.reset();
311
+ }, []);
312
+ return { ...state, submit, reset };
313
+ }
314
+ /**
315
+ * Hook for displaying and voting on polls.
316
+ *
317
+ * @example
318
+ * ```tsx
319
+ * const { poll, vote, hasVoted } = usePoll({
320
+ * siteKey: 'your-site-key',
321
+ * slug: 'favorite-framework',
322
+ * });
323
+ *
324
+ * {poll && (
325
+ * <div>
326
+ * <h3>{poll.question}</h3>
327
+ * {poll.options.map(opt => (
328
+ * <button key={opt.id} onClick={() => vote([opt.id])} disabled={hasVoted}>
329
+ * {opt.text} ({poll.results[opt.id] || 0} votes)
330
+ * </button>
331
+ * ))}
332
+ * </div>
333
+ * )}
334
+ * ```
335
+ */
336
+ export function usePoll(options) {
337
+ const controllerRef = useRef(null);
338
+ const [state, setState] = useState({
339
+ poll: null,
340
+ status: "idle",
341
+ error: null,
342
+ });
343
+ useEffect(() => {
344
+ const config = resolveConfig(options);
345
+ const controller = new PollController(config, options.slug);
346
+ controllerRef.current = controller;
347
+ const unsubscribe = controller.subscribe(setState);
348
+ // Auto-fetch on mount (default: true)
349
+ if (options.autoFetch !== false) {
350
+ controller.fetch();
351
+ }
352
+ return unsubscribe;
353
+ }, [options.siteKey, options.endpoint, options.slug]);
354
+ const vote = useCallback(async (selectedOptions) => {
355
+ await controllerRef.current?.vote(selectedOptions);
356
+ }, []);
357
+ const refresh = useCallback(async () => {
358
+ await controllerRef.current?.fetch();
359
+ }, []);
360
+ const hasVoted = controllerRef.current?.hasVoted() ?? false;
361
+ return { ...state, vote, hasVoted, refresh };
362
+ }
363
+ /**
364
+ * Hook for displaying site announcements.
365
+ *
366
+ * @example
367
+ * ```tsx
368
+ * const { announcements, dismiss } = useAnnouncements({
369
+ * siteKey: 'your-site-key',
370
+ * });
371
+ *
372
+ * {announcements.map(ann => (
373
+ * <div key={ann.id} className={`announcement-${ann.announcementType}`}>
374
+ * {ann.content}
375
+ * {ann.isDismissible && (
376
+ * <button onClick={() => dismiss(ann.id)}>Dismiss</button>
377
+ * )}
378
+ * </div>
379
+ * ))}
380
+ * ```
381
+ */
382
+ export function useAnnouncements(options) {
383
+ const controllerRef = useRef(null);
384
+ const [state, setState] = useState({
385
+ announcements: [],
386
+ dismissed: new Set(),
387
+ status: "idle",
388
+ error: null,
389
+ });
390
+ useEffect(() => {
391
+ const config = resolveConfig(options);
392
+ const controller = new AnnouncementsController(config);
393
+ controllerRef.current = controller;
394
+ const unsubscribe = controller.subscribe(setState);
395
+ // Auto-fetch on mount (default: true)
396
+ if (options.autoFetch !== false) {
397
+ controller.fetch();
398
+ }
399
+ return unsubscribe;
400
+ }, [options.siteKey, options.endpoint]);
401
+ const dismiss = useCallback(async (announcementId) => {
402
+ await controllerRef.current?.dismiss(announcementId);
403
+ }, []);
404
+ const refresh = useCallback(async () => {
405
+ await controllerRef.current?.fetch();
406
+ }, []);
407
+ // Return only visible (non-dismissed) announcements
408
+ const visibleAnnouncements = controllerRef.current?.getVisibleAnnouncements() ?? [];
409
+ return { announcements: visibleAnnouncements, status: state.status, error: state.error, dismiss, refresh };
410
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@jamwidgets/react",
3
+ "version": "0.1.0",
4
+ "description": "React hooks for Jamwidgets (forms, comments, reactions, subscriptions)",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/jamwidgets/react.git"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "homepage": "https://jamwidgets.com",
13
+ "author": "Tim Marks <tim@imothee.xyz>",
14
+ "type": "module",
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "keywords": [
27
+ "react",
28
+ "jamwidgets",
29
+ "forms",
30
+ "comments",
31
+ "reactions",
32
+ "subscribe",
33
+ "hooks"
34
+ ],
35
+ "license": "MIT",
36
+ "dependencies": {
37
+ "@jamwidgets/core": "0.1.0"
38
+ },
39
+ "peerDependencies": {
40
+ "react": "^18.0.0 || ^19.0.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/react": "^18.3.0",
44
+ "react": "^18.3.0",
45
+ "typescript": "^5.7.3"
46
+ },
47
+ "scripts": {
48
+ "build": "tsc",
49
+ "dev": "tsc --watch"
50
+ }
51
+ }