@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 +288 -0
- package/dist/index.d.ts +310 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +410 -0
- package/package.json +51 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|