@queuezero/react 0.1.3

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/dist/index.mjs ADDED
@@ -0,0 +1,403 @@
1
+ // src/context.tsx
2
+ import { createContext, useContext, useMemo, useCallback, useState, useEffect } from "react";
3
+ import { QueueZeroClient, QueueZeroStateManager } from "queuezero";
4
+ import { jsx } from "react/jsx-runtime";
5
+ var WaitlistContext = createContext(null);
6
+ function WaitlistProvider({ campaign, config, children }) {
7
+ const stateManager = useMemo(() => {
8
+ const client = new QueueZeroClient(campaign, config);
9
+ return new QueueZeroStateManager(client);
10
+ }, [campaign, config?.apiUrl, config?.baseUrl]);
11
+ const [state, setState] = useState(stateManager.getState());
12
+ useEffect(() => {
13
+ return stateManager.subscribe((newState) => {
14
+ setState(newState);
15
+ });
16
+ }, [stateManager]);
17
+ const [publicConfig, setPublicConfig] = useState(null);
18
+ const [configLoading, setConfigLoading] = useState(true);
19
+ useEffect(() => {
20
+ let mounted = true;
21
+ const fetchConfig = async () => {
22
+ try {
23
+ const cfg = await stateManager.fetchPublicConfig();
24
+ if (mounted) setPublicConfig(cfg);
25
+ } catch (e) {
26
+ console.error("Failed to fetch config", e);
27
+ } finally {
28
+ if (mounted) setConfigLoading(false);
29
+ }
30
+ };
31
+ fetchConfig();
32
+ return () => {
33
+ mounted = false;
34
+ };
35
+ }, [stateManager]);
36
+ useEffect(() => {
37
+ if (stateManager.getState().isJoined) {
38
+ stateManager.refresh();
39
+ }
40
+ }, [stateManager]);
41
+ const value = useMemo(
42
+ () => ({
43
+ loading: state.loading,
44
+ status: state.status,
45
+ error: state.error,
46
+ isJoined: state.isJoined,
47
+ campaign,
48
+ join: stateManager.join.bind(stateManager),
49
+ refresh: stateManager.refresh.bind(stateManager),
50
+ getReferralLink: stateManager.getReferralLink.bind(stateManager),
51
+ getReferralCode: stateManager.getReferralCode.bind(stateManager),
52
+ reset: stateManager.reset.bind(stateManager),
53
+ config: publicConfig,
54
+ configLoading
55
+ }),
56
+ [state, campaign, stateManager, publicConfig, configLoading]
57
+ );
58
+ return /* @__PURE__ */ jsx(WaitlistContext.Provider, { value, children });
59
+ }
60
+ function useWaitlistContext() {
61
+ const context = useContext(WaitlistContext);
62
+ if (!context) {
63
+ throw new Error("useWaitlistContext must be used within a WaitlistProvider");
64
+ }
65
+ return context;
66
+ }
67
+ function useWaitlist(campaign, config) {
68
+ const client = useMemo(() => {
69
+ return new QueueZeroClient(campaign, config);
70
+ }, [campaign, config?.apiUrl, config?.baseUrl]);
71
+ const [loading, setLoading] = useState(false);
72
+ const [status, setStatus] = useState(null);
73
+ const [error, setError] = useState(null);
74
+ const [isJoined, setIsJoined] = useState(false);
75
+ useEffect(() => {
76
+ const joined = client.isJoined();
77
+ setIsJoined(joined);
78
+ if (joined) {
79
+ refresh();
80
+ }
81
+ }, [client]);
82
+ const join = useCallback(
83
+ async (email, metadata, referrerCode) => {
84
+ setLoading(true);
85
+ setError(null);
86
+ try {
87
+ const response = await client.joinWaitlist(email, metadata, referrerCode);
88
+ const userStatus = await client.getPosition();
89
+ setStatus(userStatus);
90
+ setIsJoined(true);
91
+ return response;
92
+ } catch (err) {
93
+ setError(err instanceof Error ? err : new Error("Unknown error"));
94
+ return null;
95
+ } finally {
96
+ setLoading(false);
97
+ }
98
+ },
99
+ [client]
100
+ );
101
+ const refresh = useCallback(async () => {
102
+ if (!client.isJoined()) return null;
103
+ setLoading(true);
104
+ setError(null);
105
+ try {
106
+ const userStatus = await client.getPosition();
107
+ setStatus(userStatus);
108
+ return userStatus;
109
+ } catch (err) {
110
+ setError(err instanceof Error ? err : new Error("Unknown error"));
111
+ return null;
112
+ } finally {
113
+ setLoading(false);
114
+ }
115
+ }, [client]);
116
+ const getReferralLink = useCallback(() => client.getReferralLink(), [client]);
117
+ const getReferralCode = useCallback(() => client.getReferralCode(), [client]);
118
+ const reset = useCallback(() => {
119
+ client.reset();
120
+ setStatus(null);
121
+ setError(null);
122
+ setIsJoined(false);
123
+ }, [client]);
124
+ return { loading, status, error, isJoined, campaign, join, refresh, getReferralLink, getReferralCode, reset };
125
+ }
126
+
127
+ // src/WaitlistForm.tsx
128
+ import { useState as useState2, useCallback as useCallback2, useMemo as useMemo2 } from "react";
129
+ import { FormController } from "queuezero";
130
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
131
+ function WaitlistForm({
132
+ onSuccess,
133
+ onError,
134
+ referrerCode,
135
+ metadata,
136
+ className = "",
137
+ placeholder = "Enter your email",
138
+ buttonText = "Join Waitlist",
139
+ loadingText = "Joining...",
140
+ showLabel = false,
141
+ labelText = "Email",
142
+ children
143
+ }) {
144
+ const { join, loading, error, isJoined, config, configLoading } = useWaitlistContext();
145
+ const [email, setEmail] = useState2("");
146
+ const [formData, setFormData] = useState2({});
147
+ const [localError, setLocalError] = useState2(null);
148
+ const controller = useMemo2(
149
+ () => new FormController(join, config?.formFields || [], referrerCode),
150
+ [join, config, referrerCode]
151
+ );
152
+ const handleSubmit = useCallback2(
153
+ async (e) => {
154
+ e?.preventDefault();
155
+ setLocalError(null);
156
+ try {
157
+ const result = await controller.submit(email, formData, metadata);
158
+ if (result) {
159
+ setEmail("");
160
+ setFormData({});
161
+ onSuccess?.(result);
162
+ } else if (error) {
163
+ onError?.(error);
164
+ }
165
+ } catch (err) {
166
+ const error2 = err instanceof Error ? err : new Error("Failed to join");
167
+ setLocalError(error2);
168
+ onError?.(error2);
169
+ }
170
+ },
171
+ [email, formData, controller, metadata, onSuccess, onError, error]
172
+ );
173
+ const displayError = localError || error;
174
+ if (isJoined) {
175
+ return null;
176
+ }
177
+ if (typeof children === "function") {
178
+ return children({
179
+ email,
180
+ setEmail,
181
+ submit: handleSubmit,
182
+ loading,
183
+ error: displayError
184
+ });
185
+ }
186
+ return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: `qz-form ${className}`.trim(), children: [
187
+ showLabel && /* @__PURE__ */ jsx2("label", { htmlFor: "qz-email", className: "qz-form-label", children: labelText }),
188
+ /* @__PURE__ */ jsx2("div", { className: "qz-form-row", children: /* @__PURE__ */ jsx2(
189
+ "input",
190
+ {
191
+ id: "qz-email",
192
+ type: "email",
193
+ value: email,
194
+ onChange: (e) => setEmail(e.target.value),
195
+ placeholder,
196
+ disabled: loading,
197
+ required: true,
198
+ className: "qz-form-input",
199
+ "aria-label": !showLabel ? labelText : void 0
200
+ }
201
+ ) }),
202
+ !configLoading && config?.formFields?.map((field) => /* @__PURE__ */ jsxs("div", { className: "qz-form-row", children: [
203
+ showLabel && /* @__PURE__ */ jsx2("label", { className: "qz-form-label", children: field.label }),
204
+ field.type === "select" ? /* @__PURE__ */ jsxs(
205
+ "select",
206
+ {
207
+ value: formData[field.key] || "",
208
+ onChange: (e) => setFormData((prev) => ({ ...prev, [field.key]: e.target.value })),
209
+ disabled: loading,
210
+ required: field.required,
211
+ className: "qz-form-select",
212
+ children: [
213
+ /* @__PURE__ */ jsx2("option", { value: "", children: field.placeholder || `Select ${field.label}` }),
214
+ field.options?.map((opt) => /* @__PURE__ */ jsx2("option", { value: opt, children: opt }, opt))
215
+ ]
216
+ }
217
+ ) : /* @__PURE__ */ jsx2(
218
+ "input",
219
+ {
220
+ type: field.type === "number" ? "number" : "text",
221
+ value: formData[field.key] || "",
222
+ onChange: (e) => setFormData((prev) => ({ ...prev, [field.key]: e.target.value })),
223
+ placeholder: field.placeholder || field.label,
224
+ disabled: loading,
225
+ required: field.required,
226
+ className: "qz-form-input"
227
+ }
228
+ )
229
+ ] }, field.key)),
230
+ /* @__PURE__ */ jsx2("div", { className: "qz-form-row", children: /* @__PURE__ */ jsx2("button", { type: "submit", disabled: loading, className: "qz-form-button", children: loading ? loadingText : buttonText }) }),
231
+ displayError && /* @__PURE__ */ jsx2("div", { className: "qz-form-error", role: "alert", children: displayError.message }),
232
+ children
233
+ ] });
234
+ }
235
+
236
+ // src/WaitlistStatus.tsx
237
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
238
+ function WaitlistStatus({
239
+ className = "",
240
+ showReferrals = true,
241
+ showScore = true,
242
+ positionLabel = "Position",
243
+ scoreLabel = "Score",
244
+ referralsLabel = "Referrals",
245
+ children
246
+ }) {
247
+ const { status, loading, isJoined } = useWaitlistContext();
248
+ if (!isJoined || !status) {
249
+ return null;
250
+ }
251
+ if (loading && !status) {
252
+ return /* @__PURE__ */ jsx3("div", { className: `qz-status qz-status-loading ${className}`.trim(), children: "Loading..." });
253
+ }
254
+ if (typeof children === "function") {
255
+ return /* @__PURE__ */ jsx3(Fragment, { children: children(status) });
256
+ }
257
+ return /* @__PURE__ */ jsxs2("div", { className: `qz-status ${className}`.trim(), children: [
258
+ /* @__PURE__ */ jsxs2("div", { className: "qz-status-item qz-status-position", children: [
259
+ /* @__PURE__ */ jsx3("span", { className: "qz-status-label", children: positionLabel }),
260
+ /* @__PURE__ */ jsxs2("span", { className: "qz-status-value", children: [
261
+ "#",
262
+ status.position.toLocaleString()
263
+ ] })
264
+ ] }),
265
+ showScore && /* @__PURE__ */ jsxs2("div", { className: "qz-status-item qz-status-score", children: [
266
+ /* @__PURE__ */ jsx3("span", { className: "qz-status-label", children: scoreLabel }),
267
+ /* @__PURE__ */ jsx3("span", { className: "qz-status-value", children: status.priorityScore.toLocaleString() })
268
+ ] }),
269
+ showReferrals && /* @__PURE__ */ jsxs2("div", { className: "qz-status-item qz-status-referrals", children: [
270
+ /* @__PURE__ */ jsx3("span", { className: "qz-status-label", children: referralsLabel }),
271
+ /* @__PURE__ */ jsx3("span", { className: "qz-status-value", children: status.referralCount.toLocaleString() })
272
+ ] })
273
+ ] });
274
+ }
275
+
276
+ // src/ReferralShare.tsx
277
+ import { useState as useState3, useCallback as useCallback3 } from "react";
278
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
279
+ function ReferralShare({
280
+ className = "",
281
+ label = "Share your referral link:",
282
+ copyButtonText = "Copy",
283
+ copiedText = "Copied!",
284
+ onCopy,
285
+ shareMessage = "Join me on the waitlist!",
286
+ showSocialButtons,
287
+ children
288
+ }) {
289
+ const { getReferralLink, getReferralCode, isJoined, config } = useWaitlistContext();
290
+ const [copied, setCopied] = useState3(false);
291
+ const referralEnabled = config?.features?.referral_enabled ?? true;
292
+ const showSocial = showSocialButtons ?? config?.features?.social_share_buttons ?? false;
293
+ if (!referralEnabled) return null;
294
+ const link = getReferralLink();
295
+ const code = getReferralCode();
296
+ const handleCopy = useCallback3(async () => {
297
+ if (!link) return;
298
+ try {
299
+ await navigator.clipboard.writeText(link);
300
+ setCopied(true);
301
+ onCopy?.(link);
302
+ setTimeout(() => setCopied(false), 2e3);
303
+ } catch (err) {
304
+ const textarea = document.createElement("textarea");
305
+ textarea.value = link;
306
+ textarea.style.position = "fixed";
307
+ textarea.style.opacity = "0";
308
+ document.body.appendChild(textarea);
309
+ textarea.select();
310
+ document.execCommand("copy");
311
+ document.body.removeChild(textarea);
312
+ setCopied(true);
313
+ onCopy?.(link);
314
+ setTimeout(() => setCopied(false), 2e3);
315
+ }
316
+ }, [link, onCopy]);
317
+ const handleTwitterShare = useCallback3(() => {
318
+ if (!link) return;
319
+ const text = encodeURIComponent(`${shareMessage} ${link}`);
320
+ window.open(`https://twitter.com/intent/tweet?text=${text}`, "_blank", "noopener,noreferrer");
321
+ }, [link, shareMessage]);
322
+ const handleLinkedInShare = useCallback3(() => {
323
+ if (!link) return;
324
+ const url = encodeURIComponent(link);
325
+ window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${url}`, "_blank", "noopener,noreferrer");
326
+ }, [link]);
327
+ const handleEmailShare = useCallback3(() => {
328
+ if (!link) return;
329
+ const subject = encodeURIComponent("Join me on the waitlist!");
330
+ const body = encodeURIComponent(`${shareMessage}
331
+
332
+ ${link}`);
333
+ window.location.href = `mailto:?subject=${subject}&body=${body}`;
334
+ }, [link, shareMessage]);
335
+ if (!isJoined || !link || !code) {
336
+ return null;
337
+ }
338
+ if (typeof children === "function") {
339
+ return /* @__PURE__ */ jsx4(Fragment2, { children: children({ link, code, copy: handleCopy, copied }) });
340
+ }
341
+ return /* @__PURE__ */ jsxs3("div", { className: `qz-share ${className}`.trim(), children: [
342
+ label && /* @__PURE__ */ jsx4("div", { className: "qz-share-label", children: label }),
343
+ /* @__PURE__ */ jsxs3("div", { className: "qz-share-link-container", children: [
344
+ /* @__PURE__ */ jsx4(
345
+ "input",
346
+ {
347
+ type: "text",
348
+ value: link,
349
+ readOnly: true,
350
+ className: "qz-share-input",
351
+ onClick: (e) => e.target.select()
352
+ }
353
+ ),
354
+ /* @__PURE__ */ jsx4("button", { onClick: handleCopy, className: "qz-share-copy-button", children: copied ? copiedText : copyButtonText })
355
+ ] }),
356
+ showSocial && /* @__PURE__ */ jsxs3("div", { className: "qz-share-social", children: [
357
+ /* @__PURE__ */ jsx4(
358
+ "button",
359
+ {
360
+ onClick: handleTwitterShare,
361
+ className: "qz-share-social-button qz-share-twitter",
362
+ "aria-label": "Share on Twitter",
363
+ children: /* @__PURE__ */ jsx4("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "currentColor", children: /* @__PURE__ */ jsx4("path", { d: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" }) })
364
+ }
365
+ ),
366
+ /* @__PURE__ */ jsx4(
367
+ "button",
368
+ {
369
+ onClick: handleLinkedInShare,
370
+ className: "qz-share-social-button qz-share-linkedin",
371
+ "aria-label": "Share on LinkedIn",
372
+ children: /* @__PURE__ */ jsx4("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "currentColor", children: /* @__PURE__ */ jsx4("path", { d: "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" }) })
373
+ }
374
+ ),
375
+ /* @__PURE__ */ jsx4(
376
+ "button",
377
+ {
378
+ onClick: handleEmailShare,
379
+ className: "qz-share-social-button qz-share-email",
380
+ "aria-label": "Share via Email",
381
+ children: /* @__PURE__ */ jsxs3("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "currentColor", children: [
382
+ /* @__PURE__ */ jsx4("path", { d: "M1.5 8.67v8.58a3 3 0 003 3h15a3 3 0 003-3V8.67l-8.928 5.493a3 3 0 01-3.144 0L1.5 8.67z" }),
383
+ /* @__PURE__ */ jsx4("path", { d: "M22.5 6.908V6.75a3 3 0 00-3-3h-15a3 3 0 00-3 3v.158l9.714 5.978a1.5 1.5 0 001.572 0L22.5 6.908z" })
384
+ ] })
385
+ }
386
+ )
387
+ ] })
388
+ ] });
389
+ }
390
+
391
+ // src/index.ts
392
+ import { QueueZeroClient as QueueZeroClient2, InMemoryStorageAdapter, LocalStorageAdapter } from "queuezero";
393
+ export {
394
+ InMemoryStorageAdapter,
395
+ LocalStorageAdapter,
396
+ QueueZeroClient2 as QueueZeroClient,
397
+ ReferralShare,
398
+ WaitlistForm,
399
+ WaitlistProvider,
400
+ WaitlistStatus,
401
+ useWaitlist,
402
+ useWaitlistContext
403
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@queuezero/react",
3
+ "version": "0.1.3",
4
+ "description": "React components and hooks for QueueZero viral waitlists",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean --external react",
21
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch --external react",
22
+ "typecheck": "tsc --noEmit",
23
+ "lint": "eslint src --ext .ts,.tsx"
24
+ },
25
+ "keywords": [
26
+ "waitlist",
27
+ "viral",
28
+ "referral",
29
+ "react",
30
+ "hooks"
31
+ ],
32
+ "author": "QueueZero Team",
33
+ "license": "MIT",
34
+ "peerDependencies": {
35
+ "react": ">=17.0.0"
36
+ },
37
+ "dependencies": {
38
+ "queuezero": "^0.1.2"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^22.13.11",
42
+ "@types/react": "^19.1.6",
43
+ "react": "^19.1.0",
44
+ "tsup": "^8.4.0",
45
+ "typescript": "^5.8.2"
46
+ }
47
+ }