@pxlabz/tracey-react 0.0.1
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 +83 -0
- package/dist/LauncherTrigger-5pNAvA05.js +1056 -0
- package/dist/LauncherTrigger-5pNAvA05.js.map +1 -0
- package/dist/LauncherTrigger-DjXDXHTm.cjs +1055 -0
- package/dist/LauncherTrigger-DjXDXHTm.cjs.map +1 -0
- package/dist/__tests__/api-client.test.d.ts +2 -0
- package/dist/__tests__/api-client.test.d.ts.map +1 -0
- package/dist/__tests__/events.test.d.ts +2 -0
- package/dist/__tests__/events.test.d.ts.map +1 -0
- package/dist/__tests__/survey-logic.test.d.ts +2 -0
- package/dist/__tests__/survey-logic.test.d.ts.map +1 -0
- package/dist/components/announcements/AnnouncementBell.d.ts +9 -0
- package/dist/components/announcements/AnnouncementBell.d.ts.map +1 -0
- package/dist/components/announcements/AnnouncementList.d.ts +7 -0
- package/dist/components/announcements/AnnouncementList.d.ts.map +1 -0
- package/dist/components/announcements/AnnouncementModal.d.ts +9 -0
- package/dist/components/announcements/AnnouncementModal.d.ts.map +1 -0
- package/dist/components/announcements/headless/AnnouncementClose.d.ts +9 -0
- package/dist/components/announcements/headless/AnnouncementClose.d.ts.map +1 -0
- package/dist/components/announcements/headless/AnnouncementContent.d.ts +7 -0
- package/dist/components/announcements/headless/AnnouncementContent.d.ts.map +1 -0
- package/dist/components/announcements/headless/AnnouncementNavigation.d.ts +15 -0
- package/dist/components/announcements/headless/AnnouncementNavigation.d.ts.map +1 -0
- package/dist/components/announcements/headless/AnnouncementPortal.d.ts +7 -0
- package/dist/components/announcements/headless/AnnouncementPortal.d.ts.map +1 -0
- package/dist/components/announcements/headless/AnnouncementRoot.d.ts +24 -0
- package/dist/components/announcements/headless/AnnouncementRoot.d.ts.map +1 -0
- package/dist/components/announcements/headless/AnnouncementSlidePrimitive.d.ts +14 -0
- package/dist/components/announcements/headless/AnnouncementSlidePrimitive.d.ts.map +1 -0
- package/dist/components/announcements/headless/AnnouncementTrigger.d.ts +9 -0
- package/dist/components/announcements/headless/AnnouncementTrigger.d.ts.map +1 -0
- package/dist/components/announcements/headless/index.d.ts +8 -0
- package/dist/components/announcements/headless/index.d.ts.map +1 -0
- package/dist/components/announcements/index.d.ts +4 -0
- package/dist/components/announcements/index.d.ts.map +1 -0
- package/dist/components/feedback/FeedbackForm.d.ts +8 -0
- package/dist/components/feedback/FeedbackForm.d.ts.map +1 -0
- package/dist/components/feedback/FeedbackWidget.d.ts +9 -0
- package/dist/components/feedback/FeedbackWidget.d.ts.map +1 -0
- package/dist/components/feedback/headless/FeedbackClose.d.ts +9 -0
- package/dist/components/feedback/headless/FeedbackClose.d.ts.map +1 -0
- package/dist/components/feedback/headless/FeedbackContent.d.ts +6 -0
- package/dist/components/feedback/headless/FeedbackContent.d.ts.map +1 -0
- package/dist/components/feedback/headless/FeedbackFormPrimitive.d.ts +28 -0
- package/dist/components/feedback/headless/FeedbackFormPrimitive.d.ts.map +1 -0
- package/dist/components/feedback/headless/FeedbackPortal.d.ts +7 -0
- package/dist/components/feedback/headless/FeedbackPortal.d.ts.map +1 -0
- package/dist/components/feedback/headless/FeedbackRoot.d.ts +16 -0
- package/dist/components/feedback/headless/FeedbackRoot.d.ts.map +1 -0
- package/dist/components/feedback/headless/FeedbackTrigger.d.ts +9 -0
- package/dist/components/feedback/headless/FeedbackTrigger.d.ts.map +1 -0
- package/dist/components/feedback/headless/index.d.ts +7 -0
- package/dist/components/feedback/headless/index.d.ts.map +1 -0
- package/dist/components/feedback/index.d.ts +4 -0
- package/dist/components/feedback/index.d.ts.map +1 -0
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/launcher/TraceyLauncher.d.ts +8 -0
- package/dist/components/launcher/TraceyLauncher.d.ts.map +1 -0
- package/dist/components/launcher/headless/LauncherBadge.d.ts +8 -0
- package/dist/components/launcher/headless/LauncherBadge.d.ts.map +1 -0
- package/dist/components/launcher/headless/LauncherMenu.d.ts +6 -0
- package/dist/components/launcher/headless/LauncherMenu.d.ts.map +1 -0
- package/dist/components/launcher/headless/LauncherMenuItem.d.ts +9 -0
- package/dist/components/launcher/headless/LauncherMenuItem.d.ts.map +1 -0
- package/dist/components/launcher/headless/LauncherPanel.d.ts +6 -0
- package/dist/components/launcher/headless/LauncherPanel.d.ts.map +1 -0
- package/dist/components/launcher/headless/LauncherPortal.d.ts +7 -0
- package/dist/components/launcher/headless/LauncherPortal.d.ts.map +1 -0
- package/dist/components/launcher/headless/LauncherRoot.d.ts +21 -0
- package/dist/components/launcher/headless/LauncherRoot.d.ts.map +1 -0
- package/dist/components/launcher/headless/LauncherTab.d.ts +8 -0
- package/dist/components/launcher/headless/LauncherTab.d.ts.map +1 -0
- package/dist/components/launcher/headless/LauncherTabContent.d.ts +8 -0
- package/dist/components/launcher/headless/LauncherTabContent.d.ts.map +1 -0
- package/dist/components/launcher/headless/LauncherTrigger.d.ts +10 -0
- package/dist/components/launcher/headless/LauncherTrigger.d.ts.map +1 -0
- package/dist/components/launcher/headless/index.d.ts +10 -0
- package/dist/components/launcher/headless/index.d.ts.map +1 -0
- package/dist/components/launcher/index.d.ts +3 -0
- package/dist/components/launcher/index.d.ts.map +1 -0
- package/dist/components/surveys/SurveyWidget.d.ts +10 -0
- package/dist/components/surveys/SurveyWidget.d.ts.map +1 -0
- package/dist/components/surveys/index.d.ts +2 -0
- package/dist/components/surveys/index.d.ts.map +1 -0
- package/dist/core/api-client.d.ts +33 -0
- package/dist/core/api-client.d.ts.map +1 -0
- package/dist/core/events.d.ts +11 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/screenshot.d.ts +12 -0
- package/dist/core/screenshot.d.ts.map +1 -0
- package/dist/core/survey-logic.d.ts +7 -0
- package/dist/core/survey-logic.d.ts.map +1 -0
- package/dist/headless.cjs +100 -0
- package/dist/headless.cjs.map +1 -0
- package/dist/headless.d.ts +11 -0
- package/dist/headless.d.ts.map +1 -0
- package/dist/headless.js +101 -0
- package/dist/headless.js.map +1 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/useAnnouncements.d.ts +14 -0
- package/dist/hooks/useAnnouncements.d.ts.map +1 -0
- package/dist/hooks/useFeedback.d.ts +10 -0
- package/dist/hooks/useFeedback.d.ts.map +1 -0
- package/dist/hooks/useMyFeedback.d.ts +16 -0
- package/dist/hooks/useMyFeedback.d.ts.map +1 -0
- package/dist/hooks/useSurvey.d.ts +22 -0
- package/dist/hooks/useSurvey.d.ts.map +1 -0
- package/dist/hooks/useTracey.d.ts +3 -0
- package/dist/hooks/useTracey.d.ts.map +1 -0
- package/dist/hooks/useTraceyEvents.d.ts +14 -0
- package/dist/hooks/useTraceyEvents.d.ts.map +1 -0
- package/dist/index.cjs +4913 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4892 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/provider/MockTraceyProvider.d.ts +14 -0
- package/dist/provider/MockTraceyProvider.d.ts.map +1 -0
- package/dist/provider/TraceyProvider.d.ts +11 -0
- package/dist/provider/TraceyProvider.d.ts.map +1 -0
- package/dist/provider/context.d.ts +19 -0
- package/dist/provider/context.d.ts.map +1 -0
- package/dist/provider/index.d.ts +4 -0
- package/dist/provider/index.d.ts.map +1 -0
- package/dist/styles.css +800 -0
- package/dist/types.d.ts +188 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1,1056 @@
|
|
|
1
|
+
var __typeError = (msg) => {
|
|
2
|
+
throw TypeError(msg);
|
|
3
|
+
};
|
|
4
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
5
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
6
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
7
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
8
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
9
|
+
var _apiKey, _baseUrl, _customerId, _TraceyApiClient_instances, request_fn, _listeners;
|
|
10
|
+
import { jsx, Fragment } from "react/jsx-runtime";
|
|
11
|
+
import { createContext, useMemo, useState, useRef, useCallback, useEffect, useContext, cloneElement } from "react";
|
|
12
|
+
import { createPortal } from "react-dom";
|
|
13
|
+
const TraceyContext = createContext(null);
|
|
14
|
+
class TraceyApiClient {
|
|
15
|
+
constructor(config) {
|
|
16
|
+
__privateAdd(this, _TraceyApiClient_instances);
|
|
17
|
+
__privateAdd(this, _apiKey);
|
|
18
|
+
__privateAdd(this, _baseUrl);
|
|
19
|
+
__privateAdd(this, _customerId, null);
|
|
20
|
+
__privateSet(this, _apiKey, config.apiKey);
|
|
21
|
+
__privateSet(this, _baseUrl, config.baseUrl ?? "https://api.tracey.dev");
|
|
22
|
+
}
|
|
23
|
+
setCustomerId(id) {
|
|
24
|
+
__privateSet(this, _customerId, id);
|
|
25
|
+
}
|
|
26
|
+
getCustomerId() {
|
|
27
|
+
return __privateGet(this, _customerId);
|
|
28
|
+
}
|
|
29
|
+
async identify(user) {
|
|
30
|
+
const { userHash, ...userData } = user;
|
|
31
|
+
return __privateMethod(this, _TraceyApiClient_instances, request_fn).call(this, "/api/sdk/identify", {
|
|
32
|
+
method: "POST",
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
...userData,
|
|
35
|
+
...userHash ? { userHash } : {}
|
|
36
|
+
})
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async getAnnouncements(filter = "unread") {
|
|
40
|
+
const params = new URLSearchParams();
|
|
41
|
+
if (__privateGet(this, _customerId)) {
|
|
42
|
+
params.set("customerId", __privateGet(this, _customerId));
|
|
43
|
+
}
|
|
44
|
+
params.set("filter", filter);
|
|
45
|
+
return __privateMethod(this, _TraceyApiClient_instances, request_fn).call(this, `/api/sdk/announcements?${params.toString()}`);
|
|
46
|
+
}
|
|
47
|
+
async getUnreadCount() {
|
|
48
|
+
if (!__privateGet(this, _customerId)) {
|
|
49
|
+
throw new Error("Customer ID not set. Call identify first.");
|
|
50
|
+
}
|
|
51
|
+
return __privateMethod(this, _TraceyApiClient_instances, request_fn).call(this, `/api/sdk/announcements/unread-count?customerId=${encodeURIComponent(__privateGet(this, _customerId))}`);
|
|
52
|
+
}
|
|
53
|
+
async trackAnnouncementView(announcementId) {
|
|
54
|
+
if (!__privateGet(this, _customerId)) {
|
|
55
|
+
throw new Error("Customer ID not set. Call identify first.");
|
|
56
|
+
}
|
|
57
|
+
await __privateMethod(this, _TraceyApiClient_instances, request_fn).call(this, `/api/sdk/announcements/${announcementId}/view`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
body: JSON.stringify({ customerId: __privateGet(this, _customerId) })
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async trackAnnouncementCta(announcementId) {
|
|
63
|
+
if (!__privateGet(this, _customerId)) {
|
|
64
|
+
throw new Error("Customer ID not set. Call identify first.");
|
|
65
|
+
}
|
|
66
|
+
await __privateMethod(this, _TraceyApiClient_instances, request_fn).call(this, `/api/sdk/announcements/${announcementId}/cta`, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
body: JSON.stringify({ customerId: __privateGet(this, _customerId) })
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async submitFeedback(feedback) {
|
|
72
|
+
return __privateMethod(this, _TraceyApiClient_instances, request_fn).call(this, "/api/sdk/feedback", {
|
|
73
|
+
method: "POST",
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
...feedback,
|
|
76
|
+
customerId: __privateGet(this, _customerId) ?? void 0
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async getMyFeedback(params) {
|
|
81
|
+
if (!__privateGet(this, _customerId)) {
|
|
82
|
+
throw new Error("Customer ID not set. Call identify first.");
|
|
83
|
+
}
|
|
84
|
+
const searchParams = new URLSearchParams();
|
|
85
|
+
searchParams.set("customerId", __privateGet(this, _customerId));
|
|
86
|
+
if ((params == null ? void 0 : params.limit) !== void 0) {
|
|
87
|
+
searchParams.set("limit", String(params.limit));
|
|
88
|
+
}
|
|
89
|
+
if ((params == null ? void 0 : params.offset) !== void 0) {
|
|
90
|
+
searchParams.set("offset", String(params.offset));
|
|
91
|
+
}
|
|
92
|
+
return __privateMethod(this, _TraceyApiClient_instances, request_fn).call(this, `/api/sdk/feedback/mine?${searchParams.toString()}`);
|
|
93
|
+
}
|
|
94
|
+
async getActiveSurvey(context) {
|
|
95
|
+
const params = new URLSearchParams();
|
|
96
|
+
if (__privateGet(this, _customerId)) params.set("customerId", __privateGet(this, _customerId));
|
|
97
|
+
if (context == null ? void 0 : context.anonymousSessionId) {
|
|
98
|
+
params.set("anonymousSessionId", context.anonymousSessionId);
|
|
99
|
+
}
|
|
100
|
+
if (context == null ? void 0 : context.url) params.set("url", context.url);
|
|
101
|
+
const query = params.toString();
|
|
102
|
+
const response = await __privateMethod(this, _TraceyApiClient_instances, request_fn).call(this, `/api/sdk/surveys${query ? `?${query}` : ""}`);
|
|
103
|
+
return response.survey;
|
|
104
|
+
}
|
|
105
|
+
async submitSurveyResponse(surveyId, payload) {
|
|
106
|
+
return __privateMethod(this, _TraceyApiClient_instances, request_fn).call(this, `/api/sdk/surveys/${surveyId}/responses`, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
customerId: __privateGet(this, _customerId) ?? void 0,
|
|
110
|
+
anonymousSessionId: payload.anonymousSessionId,
|
|
111
|
+
respondentEmail: payload.respondentEmail,
|
|
112
|
+
respondentName: payload.respondentName,
|
|
113
|
+
metadata: payload.metadata,
|
|
114
|
+
answers: payload.answers
|
|
115
|
+
})
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
_apiKey = new WeakMap();
|
|
120
|
+
_baseUrl = new WeakMap();
|
|
121
|
+
_customerId = new WeakMap();
|
|
122
|
+
_TraceyApiClient_instances = new WeakSet();
|
|
123
|
+
request_fn = async function(path, options = {}) {
|
|
124
|
+
const url = `${__privateGet(this, _baseUrl)}${path}`;
|
|
125
|
+
const headers = new Headers(options.headers);
|
|
126
|
+
headers.set("Authorization", `Bearer ${__privateGet(this, _apiKey)}`);
|
|
127
|
+
headers.set("Content-Type", "application/json");
|
|
128
|
+
const response = await fetch(url, {
|
|
129
|
+
...options,
|
|
130
|
+
headers
|
|
131
|
+
});
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
const error = await response.json().catch(() => ({
|
|
134
|
+
message: `HTTP ${response.status}: ${response.statusText}`
|
|
135
|
+
}));
|
|
136
|
+
throw new Error(error.message);
|
|
137
|
+
}
|
|
138
|
+
return response.json();
|
|
139
|
+
};
|
|
140
|
+
class TraceyEventEmitter {
|
|
141
|
+
constructor() {
|
|
142
|
+
__privateAdd(this, _listeners, /* @__PURE__ */ new Map());
|
|
143
|
+
}
|
|
144
|
+
subscribe(event, callback) {
|
|
145
|
+
if (!__privateGet(this, _listeners).has(event)) {
|
|
146
|
+
__privateGet(this, _listeners).set(event, /* @__PURE__ */ new Set());
|
|
147
|
+
}
|
|
148
|
+
const listeners = __privateGet(this, _listeners).get(event);
|
|
149
|
+
listeners.add(callback);
|
|
150
|
+
return () => {
|
|
151
|
+
listeners.delete(callback);
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
unsubscribe(event, callback) {
|
|
155
|
+
const listeners = __privateGet(this, _listeners).get(event);
|
|
156
|
+
if (listeners) {
|
|
157
|
+
listeners.delete(callback);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
emit(event, data) {
|
|
161
|
+
const listeners = __privateGet(this, _listeners).get(event);
|
|
162
|
+
if (listeners) {
|
|
163
|
+
listeners.forEach((callback) => {
|
|
164
|
+
try {
|
|
165
|
+
callback(data);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error(`Error in event listener for ${event}:`, error);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
clear() {
|
|
173
|
+
__privateGet(this, _listeners).clear();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
_listeners = new WeakMap();
|
|
177
|
+
const UNREAD_POLL_INTERVAL_MS = 5 * 60 * 1e3;
|
|
178
|
+
function getUnreadCacheKey(apiKey, userId) {
|
|
179
|
+
return `tracey:unread:${apiKey}:${userId}`;
|
|
180
|
+
}
|
|
181
|
+
function readCachedUnreadCount(apiKey, userId) {
|
|
182
|
+
try {
|
|
183
|
+
const raw = localStorage.getItem(getUnreadCacheKey(apiKey, userId));
|
|
184
|
+
if (raw !== null) {
|
|
185
|
+
const parsed = parseInt(raw, 10);
|
|
186
|
+
return isNaN(parsed) ? 0 : parsed;
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
function writeCachedUnreadCount(apiKey, userId, count) {
|
|
193
|
+
try {
|
|
194
|
+
localStorage.setItem(getUnreadCacheKey(apiKey, userId), String(count));
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function TraceyProvider({
|
|
199
|
+
apiKey,
|
|
200
|
+
baseUrl,
|
|
201
|
+
user,
|
|
202
|
+
children,
|
|
203
|
+
debug = false
|
|
204
|
+
}) {
|
|
205
|
+
const config = useMemo(
|
|
206
|
+
() => ({ apiKey, baseUrl, debug }),
|
|
207
|
+
[apiKey, baseUrl, debug]
|
|
208
|
+
);
|
|
209
|
+
const client = useMemo(() => new TraceyApiClient(config), [config]);
|
|
210
|
+
const events = useMemo(() => new TraceyEventEmitter(), []);
|
|
211
|
+
const [isIdentified, setIsIdentified] = useState(false);
|
|
212
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
213
|
+
const [error, setError] = useState(null);
|
|
214
|
+
const [announcements, setAnnouncements] = useState([]);
|
|
215
|
+
const [announcementsLoading, setAnnouncementsLoading] = useState(false);
|
|
216
|
+
const [unreadCount, setUnreadCountState] = useState(
|
|
217
|
+
() => readCachedUnreadCount(apiKey, user.id)
|
|
218
|
+
);
|
|
219
|
+
const unreadCountRef = useRef(unreadCount);
|
|
220
|
+
const setUnreadCount = useCallback(
|
|
221
|
+
(count) => {
|
|
222
|
+
const previousCount = unreadCountRef.current;
|
|
223
|
+
unreadCountRef.current = count;
|
|
224
|
+
setUnreadCountState(count);
|
|
225
|
+
writeCachedUnreadCount(apiKey, user.id, count);
|
|
226
|
+
if (count !== previousCount) {
|
|
227
|
+
events.emit("unread-count-changed", {
|
|
228
|
+
count,
|
|
229
|
+
previousCount
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
[apiKey, user.id, events]
|
|
234
|
+
);
|
|
235
|
+
const fetchUnreadCount = useCallback(async () => {
|
|
236
|
+
try {
|
|
237
|
+
const { count } = await client.getUnreadCount();
|
|
238
|
+
setUnreadCount(count);
|
|
239
|
+
} catch {
|
|
240
|
+
}
|
|
241
|
+
}, [client, setUnreadCount]);
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
let mounted = true;
|
|
244
|
+
const identify = async () => {
|
|
245
|
+
setIsLoading(true);
|
|
246
|
+
setError(null);
|
|
247
|
+
try {
|
|
248
|
+
await client.identify(user);
|
|
249
|
+
client.setCustomerId(user.id);
|
|
250
|
+
if (mounted) {
|
|
251
|
+
setIsIdentified(true);
|
|
252
|
+
events.emit("user:identified", { user });
|
|
253
|
+
}
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if (mounted) {
|
|
256
|
+
const error2 = err instanceof Error ? err : new Error("Failed to identify user");
|
|
257
|
+
setError(error2);
|
|
258
|
+
events.emit("error", { error: error2, context: "identify" });
|
|
259
|
+
}
|
|
260
|
+
} finally {
|
|
261
|
+
if (mounted) {
|
|
262
|
+
setIsLoading(false);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
identify();
|
|
267
|
+
return () => {
|
|
268
|
+
mounted = false;
|
|
269
|
+
};
|
|
270
|
+
}, [client, user, events]);
|
|
271
|
+
const fetchAnnouncements = useCallback(async () => {
|
|
272
|
+
if (!isIdentified) return;
|
|
273
|
+
setAnnouncementsLoading(true);
|
|
274
|
+
try {
|
|
275
|
+
const data = await client.getAnnouncements();
|
|
276
|
+
setAnnouncements(data);
|
|
277
|
+
} catch (err) {
|
|
278
|
+
const error2 = err instanceof Error ? err : new Error("Failed to fetch announcements");
|
|
279
|
+
events.emit("error", { error: error2, context: "announcements" });
|
|
280
|
+
} finally {
|
|
281
|
+
setAnnouncementsLoading(false);
|
|
282
|
+
}
|
|
283
|
+
}, [client, events, isIdentified]);
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
if (isIdentified) {
|
|
286
|
+
fetchAnnouncements();
|
|
287
|
+
fetchUnreadCount();
|
|
288
|
+
}
|
|
289
|
+
}, [isIdentified, fetchAnnouncements, fetchUnreadCount]);
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
if (!isIdentified) return;
|
|
292
|
+
const interval = setInterval(fetchUnreadCount, UNREAD_POLL_INTERVAL_MS);
|
|
293
|
+
return () => clearInterval(interval);
|
|
294
|
+
}, [isIdentified, fetchUnreadCount]);
|
|
295
|
+
const contextValue = useMemo(
|
|
296
|
+
() => ({
|
|
297
|
+
config,
|
|
298
|
+
client,
|
|
299
|
+
events,
|
|
300
|
+
user: isIdentified ? user : null,
|
|
301
|
+
isIdentified,
|
|
302
|
+
isLoading,
|
|
303
|
+
error,
|
|
304
|
+
announcements,
|
|
305
|
+
announcementsLoading,
|
|
306
|
+
refetchAnnouncements: fetchAnnouncements,
|
|
307
|
+
unreadCount,
|
|
308
|
+
setUnreadCount
|
|
309
|
+
}),
|
|
310
|
+
[
|
|
311
|
+
config,
|
|
312
|
+
client,
|
|
313
|
+
events,
|
|
314
|
+
user,
|
|
315
|
+
isIdentified,
|
|
316
|
+
isLoading,
|
|
317
|
+
error,
|
|
318
|
+
announcements,
|
|
319
|
+
announcementsLoading,
|
|
320
|
+
fetchAnnouncements,
|
|
321
|
+
unreadCount,
|
|
322
|
+
setUnreadCount
|
|
323
|
+
]
|
|
324
|
+
);
|
|
325
|
+
return /* @__PURE__ */ jsx(TraceyContext.Provider, { value: contextValue, children });
|
|
326
|
+
}
|
|
327
|
+
const defaultMockUser = {
|
|
328
|
+
id: "mock-user-1",
|
|
329
|
+
email: "demo@example.com",
|
|
330
|
+
name: "Demo User",
|
|
331
|
+
role: "user"
|
|
332
|
+
};
|
|
333
|
+
const defaultMockAnnouncements = [
|
|
334
|
+
{
|
|
335
|
+
id: 1,
|
|
336
|
+
title: "Welcome to Our App!",
|
|
337
|
+
status: "published",
|
|
338
|
+
displayType: "modal",
|
|
339
|
+
publishAt: null,
|
|
340
|
+
expiresAt: null,
|
|
341
|
+
slides: [
|
|
342
|
+
{
|
|
343
|
+
id: 1,
|
|
344
|
+
order: 0,
|
|
345
|
+
title: "Getting Started",
|
|
346
|
+
body: "Here's how to get the most out of our platform.",
|
|
347
|
+
imageUrl: null,
|
|
348
|
+
videoUrl: null,
|
|
349
|
+
ctaText: "Learn More",
|
|
350
|
+
ctaUrl: "https://example.com/docs"
|
|
351
|
+
}
|
|
352
|
+
],
|
|
353
|
+
viewed: false,
|
|
354
|
+
ctaClicked: false
|
|
355
|
+
}
|
|
356
|
+
];
|
|
357
|
+
function MockTraceyProvider({
|
|
358
|
+
children,
|
|
359
|
+
mockUser = defaultMockUser,
|
|
360
|
+
mockAnnouncements = defaultMockAnnouncements
|
|
361
|
+
}) {
|
|
362
|
+
const config = useMemo(
|
|
363
|
+
() => ({
|
|
364
|
+
apiKey: "mock-api-key",
|
|
365
|
+
baseUrl: "https://mock.tracey.dev",
|
|
366
|
+
debug: true
|
|
367
|
+
}),
|
|
368
|
+
[]
|
|
369
|
+
);
|
|
370
|
+
const client = useMemo(() => {
|
|
371
|
+
const apiClient = new TraceyApiClient(config);
|
|
372
|
+
apiClient.setCustomerId(mockUser.id);
|
|
373
|
+
apiClient.trackAnnouncementView = async () => {
|
|
374
|
+
};
|
|
375
|
+
apiClient.trackAnnouncementCta = async () => {
|
|
376
|
+
};
|
|
377
|
+
apiClient.submitFeedback = async (feedback) => ({
|
|
378
|
+
id: Date.now(),
|
|
379
|
+
title: feedback.title,
|
|
380
|
+
description: feedback.description ?? null,
|
|
381
|
+
type: feedback.type,
|
|
382
|
+
status: "new",
|
|
383
|
+
category: feedback.category ?? null,
|
|
384
|
+
tags: feedback.tags ?? null,
|
|
385
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
386
|
+
});
|
|
387
|
+
apiClient.identify = async () => ({ success: true });
|
|
388
|
+
apiClient.getAnnouncements = async () => mockAnnouncements;
|
|
389
|
+
apiClient.getUnreadCount = async () => ({
|
|
390
|
+
count: mockAnnouncements.filter((a) => !a.viewed).length
|
|
391
|
+
});
|
|
392
|
+
apiClient.getActiveSurvey = async () => null;
|
|
393
|
+
apiClient.submitSurveyResponse = async () => ({
|
|
394
|
+
success: true,
|
|
395
|
+
responseId: Date.now()
|
|
396
|
+
});
|
|
397
|
+
return apiClient;
|
|
398
|
+
}, [config, mockUser, mockAnnouncements]);
|
|
399
|
+
const events = useMemo(() => new TraceyEventEmitter(), []);
|
|
400
|
+
const [announcements, setAnnouncements] = useState(mockAnnouncements);
|
|
401
|
+
const [unreadCount, setUnreadCount] = useState(
|
|
402
|
+
() => mockAnnouncements.filter((a) => !a.viewed).length
|
|
403
|
+
);
|
|
404
|
+
const refetchAnnouncements = useCallback(async () => {
|
|
405
|
+
setAnnouncements(mockAnnouncements);
|
|
406
|
+
}, [mockAnnouncements]);
|
|
407
|
+
const contextValue = useMemo(
|
|
408
|
+
() => ({
|
|
409
|
+
config,
|
|
410
|
+
client,
|
|
411
|
+
events,
|
|
412
|
+
user: mockUser,
|
|
413
|
+
isIdentified: true,
|
|
414
|
+
isLoading: false,
|
|
415
|
+
error: null,
|
|
416
|
+
announcements,
|
|
417
|
+
announcementsLoading: false,
|
|
418
|
+
refetchAnnouncements,
|
|
419
|
+
unreadCount,
|
|
420
|
+
setUnreadCount
|
|
421
|
+
}),
|
|
422
|
+
[
|
|
423
|
+
config,
|
|
424
|
+
client,
|
|
425
|
+
events,
|
|
426
|
+
mockUser,
|
|
427
|
+
announcements,
|
|
428
|
+
refetchAnnouncements,
|
|
429
|
+
unreadCount
|
|
430
|
+
]
|
|
431
|
+
);
|
|
432
|
+
return /* @__PURE__ */ jsx(TraceyContext.Provider, { value: contextValue, children });
|
|
433
|
+
}
|
|
434
|
+
function useTracey() {
|
|
435
|
+
const context = useContext(TraceyContext);
|
|
436
|
+
if (!context) {
|
|
437
|
+
throw new Error("useTracey must be used within a TraceyProvider");
|
|
438
|
+
}
|
|
439
|
+
return context;
|
|
440
|
+
}
|
|
441
|
+
const MAX_SUBMISSIONS_PER_MINUTE = 5;
|
|
442
|
+
const RATE_LIMIT_WINDOW_MS = 6e4;
|
|
443
|
+
function useFeedback() {
|
|
444
|
+
const { client, events } = useTracey();
|
|
445
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
446
|
+
const [error, setError] = useState(null);
|
|
447
|
+
const [lastSubmission, setLastSubmission] = useState(
|
|
448
|
+
null
|
|
449
|
+
);
|
|
450
|
+
const submissionTimestamps = useRef([]);
|
|
451
|
+
const submit = useCallback(
|
|
452
|
+
async (feedback) => {
|
|
453
|
+
const now = Date.now();
|
|
454
|
+
submissionTimestamps.current = submissionTimestamps.current.filter(
|
|
455
|
+
(ts) => now - ts < RATE_LIMIT_WINDOW_MS
|
|
456
|
+
);
|
|
457
|
+
if (submissionTimestamps.current.length >= MAX_SUBMISSIONS_PER_MINUTE) {
|
|
458
|
+
const oldestInWindow = submissionTimestamps.current[0];
|
|
459
|
+
const retryAfterMs = RATE_LIMIT_WINDOW_MS - (now - oldestInWindow);
|
|
460
|
+
const rateLimitError = new Error(
|
|
461
|
+
"Too many submissions. Please wait before submitting again."
|
|
462
|
+
);
|
|
463
|
+
setError(rateLimitError);
|
|
464
|
+
events.emit("feedback:rate-limited", { retryAfterMs });
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
setIsSubmitting(true);
|
|
468
|
+
setError(null);
|
|
469
|
+
try {
|
|
470
|
+
const response = await client.submitFeedback(feedback);
|
|
471
|
+
submissionTimestamps.current.push(Date.now());
|
|
472
|
+
setLastSubmission(response);
|
|
473
|
+
events.emit("feedback:submitted", { feedback: response });
|
|
474
|
+
return response;
|
|
475
|
+
} catch (err) {
|
|
476
|
+
const error2 = err instanceof Error ? err : new Error("Failed to submit feedback");
|
|
477
|
+
setError(error2);
|
|
478
|
+
events.emit("feedback:error", { error: error2 });
|
|
479
|
+
return null;
|
|
480
|
+
} finally {
|
|
481
|
+
setIsSubmitting(false);
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
[client, events]
|
|
485
|
+
);
|
|
486
|
+
const reset = useCallback(() => {
|
|
487
|
+
setError(null);
|
|
488
|
+
setLastSubmission(null);
|
|
489
|
+
}, []);
|
|
490
|
+
return {
|
|
491
|
+
submit,
|
|
492
|
+
isSubmitting,
|
|
493
|
+
error,
|
|
494
|
+
lastSubmission,
|
|
495
|
+
reset
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function useAnnouncements() {
|
|
499
|
+
const {
|
|
500
|
+
client,
|
|
501
|
+
events,
|
|
502
|
+
announcements,
|
|
503
|
+
announcementsLoading,
|
|
504
|
+
refetchAnnouncements,
|
|
505
|
+
unreadCount,
|
|
506
|
+
setUnreadCount
|
|
507
|
+
} = useTracey();
|
|
508
|
+
const unread = useMemo(
|
|
509
|
+
() => announcements.filter((a) => !a.viewed),
|
|
510
|
+
[announcements]
|
|
511
|
+
);
|
|
512
|
+
const markAsViewed = useCallback(
|
|
513
|
+
async (announcementId) => {
|
|
514
|
+
try {
|
|
515
|
+
await client.trackAnnouncementView(announcementId);
|
|
516
|
+
const announcement = announcements.find((a) => a.id === announcementId);
|
|
517
|
+
if (announcement) {
|
|
518
|
+
events.emit("announcement:viewed", { announcement });
|
|
519
|
+
}
|
|
520
|
+
setUnreadCount(Math.max(0, unreadCount - 1));
|
|
521
|
+
await refetchAnnouncements();
|
|
522
|
+
} catch (err) {
|
|
523
|
+
const error = err instanceof Error ? err : new Error("Failed to mark announcement as viewed");
|
|
524
|
+
events.emit("error", { error, context: "announcement:view" });
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
[
|
|
528
|
+
client,
|
|
529
|
+
events,
|
|
530
|
+
announcements,
|
|
531
|
+
refetchAnnouncements,
|
|
532
|
+
unreadCount,
|
|
533
|
+
setUnreadCount
|
|
534
|
+
]
|
|
535
|
+
);
|
|
536
|
+
const markAsRead = useCallback(
|
|
537
|
+
async (announcementId) => {
|
|
538
|
+
await markAsViewed(announcementId);
|
|
539
|
+
},
|
|
540
|
+
[markAsViewed]
|
|
541
|
+
);
|
|
542
|
+
const markAllAsRead = useCallback(async () => {
|
|
543
|
+
const unreadAnnouncements = announcements.filter((a) => !a.viewed);
|
|
544
|
+
if (unreadAnnouncements.length === 0) return;
|
|
545
|
+
setUnreadCount(0);
|
|
546
|
+
try {
|
|
547
|
+
await Promise.all(
|
|
548
|
+
unreadAnnouncements.map((a) => client.trackAnnouncementView(a.id))
|
|
549
|
+
);
|
|
550
|
+
await refetchAnnouncements();
|
|
551
|
+
} catch (err) {
|
|
552
|
+
const error = err instanceof Error ? err : new Error("Failed to mark all announcements as read");
|
|
553
|
+
events.emit("error", { error, context: "announcement:mark-all-read" });
|
|
554
|
+
setUnreadCount(unreadAnnouncements.length);
|
|
555
|
+
}
|
|
556
|
+
}, [announcements, client, events, refetchAnnouncements, setUnreadCount]);
|
|
557
|
+
const markCtaClicked = useCallback(
|
|
558
|
+
async (announcementId, slideIndex) => {
|
|
559
|
+
try {
|
|
560
|
+
await client.trackAnnouncementCta(announcementId);
|
|
561
|
+
const announcement = announcements.find((a) => a.id === announcementId);
|
|
562
|
+
if (announcement) {
|
|
563
|
+
events.emit("announcement:cta-clicked", {
|
|
564
|
+
announcement,
|
|
565
|
+
slideIndex
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
} catch (err) {
|
|
569
|
+
const error = err instanceof Error ? err : new Error("Failed to track CTA click");
|
|
570
|
+
events.emit("error", { error, context: "announcement:cta" });
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
[client, events, announcements]
|
|
574
|
+
);
|
|
575
|
+
return {
|
|
576
|
+
announcements,
|
|
577
|
+
unread,
|
|
578
|
+
unreadCount,
|
|
579
|
+
isLoading: announcementsLoading,
|
|
580
|
+
markAsViewed,
|
|
581
|
+
markAsRead,
|
|
582
|
+
markAllAsRead,
|
|
583
|
+
markCtaClicked,
|
|
584
|
+
refetch: refetchAnnouncements
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function useTraceyEvents() {
|
|
588
|
+
const { events } = useTracey();
|
|
589
|
+
const subscribe = useCallback(
|
|
590
|
+
(event, callback) => {
|
|
591
|
+
return events.subscribe(event, callback);
|
|
592
|
+
},
|
|
593
|
+
[events]
|
|
594
|
+
);
|
|
595
|
+
const emit = useCallback(
|
|
596
|
+
(event, data) => {
|
|
597
|
+
events.emit(event, data);
|
|
598
|
+
},
|
|
599
|
+
[events]
|
|
600
|
+
);
|
|
601
|
+
return { subscribe, emit };
|
|
602
|
+
}
|
|
603
|
+
const FeedbackContext = createContext(null);
|
|
604
|
+
function useFeedbackContext() {
|
|
605
|
+
const context = useContext(FeedbackContext);
|
|
606
|
+
if (!context) {
|
|
607
|
+
throw new Error(
|
|
608
|
+
"Feedback compound components must be used within FeedbackRoot"
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
return context;
|
|
612
|
+
}
|
|
613
|
+
function FeedbackRoot({
|
|
614
|
+
children,
|
|
615
|
+
defaultOpen = false,
|
|
616
|
+
open,
|
|
617
|
+
onOpenChange
|
|
618
|
+
}) {
|
|
619
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
620
|
+
const isOpen = open ?? internalOpen;
|
|
621
|
+
const setIsOpen = useCallback(
|
|
622
|
+
(newOpen) => {
|
|
623
|
+
if (open === void 0) {
|
|
624
|
+
setInternalOpen(newOpen);
|
|
625
|
+
}
|
|
626
|
+
onOpenChange == null ? void 0 : onOpenChange(newOpen);
|
|
627
|
+
},
|
|
628
|
+
[open, onOpenChange]
|
|
629
|
+
);
|
|
630
|
+
const toggle = useCallback(() => {
|
|
631
|
+
setIsOpen(!isOpen);
|
|
632
|
+
}, [isOpen, setIsOpen]);
|
|
633
|
+
return /* @__PURE__ */ jsx(FeedbackContext.Provider, { value: { isOpen, setIsOpen, toggle }, children });
|
|
634
|
+
}
|
|
635
|
+
function FeedbackClose({
|
|
636
|
+
children,
|
|
637
|
+
asChild = true
|
|
638
|
+
}) {
|
|
639
|
+
const { setIsOpen } = useFeedbackContext();
|
|
640
|
+
const handleClose = (e) => {
|
|
641
|
+
var _a, _b;
|
|
642
|
+
(_b = (_a = children.props).onClick) == null ? void 0 : _b.call(_a, e);
|
|
643
|
+
setIsOpen(false);
|
|
644
|
+
};
|
|
645
|
+
if (asChild) {
|
|
646
|
+
return cloneElement(children, { onClick: handleClose });
|
|
647
|
+
}
|
|
648
|
+
return /* @__PURE__ */ jsx("button", { type: "button", onClick: handleClose, children });
|
|
649
|
+
}
|
|
650
|
+
function FeedbackFormPrimitive({
|
|
651
|
+
children,
|
|
652
|
+
onSuccess,
|
|
653
|
+
closeOnSuccess = true
|
|
654
|
+
}) {
|
|
655
|
+
const { setIsOpen } = useFeedbackContext();
|
|
656
|
+
const { submit, isSubmitting, error } = useFeedback();
|
|
657
|
+
const [title, setTitle] = useState("");
|
|
658
|
+
const [description, setDescription] = useState("");
|
|
659
|
+
const [type, setType] = useState("general");
|
|
660
|
+
const [screenshot, setScreenshot] = useState(null);
|
|
661
|
+
const handleSubmit = useCallback(
|
|
662
|
+
async (e) => {
|
|
663
|
+
e.preventDefault();
|
|
664
|
+
const result = await submit({
|
|
665
|
+
title,
|
|
666
|
+
description: description || void 0,
|
|
667
|
+
type,
|
|
668
|
+
screenshot: screenshot ?? void 0
|
|
669
|
+
});
|
|
670
|
+
if (result) {
|
|
671
|
+
setTitle("");
|
|
672
|
+
setDescription("");
|
|
673
|
+
setType("general");
|
|
674
|
+
setScreenshot(null);
|
|
675
|
+
onSuccess == null ? void 0 : onSuccess();
|
|
676
|
+
if (closeOnSuccess) {
|
|
677
|
+
setIsOpen(false);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
[
|
|
682
|
+
submit,
|
|
683
|
+
title,
|
|
684
|
+
description,
|
|
685
|
+
type,
|
|
686
|
+
screenshot,
|
|
687
|
+
onSuccess,
|
|
688
|
+
closeOnSuccess,
|
|
689
|
+
setIsOpen
|
|
690
|
+
]
|
|
691
|
+
);
|
|
692
|
+
return /* @__PURE__ */ jsx(Fragment, { children: children({
|
|
693
|
+
title,
|
|
694
|
+
setTitle,
|
|
695
|
+
description,
|
|
696
|
+
setDescription,
|
|
697
|
+
type,
|
|
698
|
+
setType,
|
|
699
|
+
screenshot,
|
|
700
|
+
setScreenshot,
|
|
701
|
+
isSubmitting,
|
|
702
|
+
error,
|
|
703
|
+
handleSubmit
|
|
704
|
+
}) });
|
|
705
|
+
}
|
|
706
|
+
function FeedbackContent({ children, ...props }) {
|
|
707
|
+
const { isOpen } = useFeedbackContext();
|
|
708
|
+
if (!isOpen) {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
return /* @__PURE__ */ jsx("div", { role: "dialog", "aria-modal": "true", ...props, children });
|
|
712
|
+
}
|
|
713
|
+
function FeedbackPortal({ children, container }) {
|
|
714
|
+
const [mounted, setMounted] = useState(false);
|
|
715
|
+
useEffect(() => {
|
|
716
|
+
setMounted(true);
|
|
717
|
+
}, []);
|
|
718
|
+
if (!mounted) {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
const target = container ?? document.body;
|
|
722
|
+
return createPortal(children, target);
|
|
723
|
+
}
|
|
724
|
+
function FeedbackTrigger({
|
|
725
|
+
children,
|
|
726
|
+
asChild = true
|
|
727
|
+
}) {
|
|
728
|
+
const { toggle } = useFeedbackContext();
|
|
729
|
+
if (asChild) {
|
|
730
|
+
return cloneElement(children, {
|
|
731
|
+
onClick: (e) => {
|
|
732
|
+
var _a, _b;
|
|
733
|
+
(_b = (_a = children.props).onClick) == null ? void 0 : _b.call(_a, e);
|
|
734
|
+
toggle();
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
return /* @__PURE__ */ jsx("button", { type: "button", onClick: toggle, children });
|
|
739
|
+
}
|
|
740
|
+
const AnnouncementContext = createContext(
|
|
741
|
+
null
|
|
742
|
+
);
|
|
743
|
+
function useAnnouncementContext() {
|
|
744
|
+
const context = useContext(AnnouncementContext);
|
|
745
|
+
if (!context) {
|
|
746
|
+
throw new Error(
|
|
747
|
+
"Announcement compound components must be used within AnnouncementRoot"
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
return context;
|
|
751
|
+
}
|
|
752
|
+
function AnnouncementRoot({
|
|
753
|
+
children,
|
|
754
|
+
announcement = null,
|
|
755
|
+
defaultOpen = false,
|
|
756
|
+
open,
|
|
757
|
+
onOpenChange
|
|
758
|
+
}) {
|
|
759
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
760
|
+
const [currentAnnouncement, setCurrentAnnouncement] = useState(announcement);
|
|
761
|
+
const [currentSlideIndex, setCurrentSlideIndex] = useState(0);
|
|
762
|
+
useEffect(() => {
|
|
763
|
+
setCurrentAnnouncement(announcement);
|
|
764
|
+
setCurrentSlideIndex(0);
|
|
765
|
+
}, [announcement]);
|
|
766
|
+
const isOpen = open ?? internalOpen;
|
|
767
|
+
const totalSlides = (currentAnnouncement == null ? void 0 : currentAnnouncement.slides.length) ?? 0;
|
|
768
|
+
const setIsOpen = useCallback(
|
|
769
|
+
(newOpen) => {
|
|
770
|
+
if (open === void 0) {
|
|
771
|
+
setInternalOpen(newOpen);
|
|
772
|
+
}
|
|
773
|
+
onOpenChange == null ? void 0 : onOpenChange(newOpen);
|
|
774
|
+
},
|
|
775
|
+
[open, onOpenChange]
|
|
776
|
+
);
|
|
777
|
+
const nextSlide = useCallback(() => {
|
|
778
|
+
if (currentSlideIndex < totalSlides - 1) {
|
|
779
|
+
setCurrentSlideIndex((prev) => prev + 1);
|
|
780
|
+
}
|
|
781
|
+
}, [currentSlideIndex, totalSlides]);
|
|
782
|
+
const prevSlide = useCallback(() => {
|
|
783
|
+
if (currentSlideIndex > 0) {
|
|
784
|
+
setCurrentSlideIndex((prev) => prev - 1);
|
|
785
|
+
}
|
|
786
|
+
}, [currentSlideIndex]);
|
|
787
|
+
return /* @__PURE__ */ jsx(
|
|
788
|
+
AnnouncementContext.Provider,
|
|
789
|
+
{
|
|
790
|
+
value: {
|
|
791
|
+
isOpen,
|
|
792
|
+
setIsOpen,
|
|
793
|
+
currentAnnouncement,
|
|
794
|
+
setCurrentAnnouncement,
|
|
795
|
+
currentSlideIndex,
|
|
796
|
+
setCurrentSlideIndex,
|
|
797
|
+
nextSlide,
|
|
798
|
+
prevSlide,
|
|
799
|
+
totalSlides
|
|
800
|
+
},
|
|
801
|
+
children
|
|
802
|
+
}
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
function AnnouncementClose({
|
|
806
|
+
children,
|
|
807
|
+
asChild = true
|
|
808
|
+
}) {
|
|
809
|
+
const { setIsOpen } = useAnnouncementContext();
|
|
810
|
+
const handleClose = (e) => {
|
|
811
|
+
var _a, _b;
|
|
812
|
+
(_b = (_a = children.props).onClick) == null ? void 0 : _b.call(_a, e);
|
|
813
|
+
setIsOpen(false);
|
|
814
|
+
};
|
|
815
|
+
if (asChild) {
|
|
816
|
+
return cloneElement(children, { onClick: handleClose });
|
|
817
|
+
}
|
|
818
|
+
return /* @__PURE__ */ jsx("button", { type: "button", onClick: handleClose, children });
|
|
819
|
+
}
|
|
820
|
+
function AnnouncementContent({
|
|
821
|
+
children,
|
|
822
|
+
trackView = true,
|
|
823
|
+
...props
|
|
824
|
+
}) {
|
|
825
|
+
const { isOpen, currentAnnouncement } = useAnnouncementContext();
|
|
826
|
+
const { markAsViewed } = useAnnouncements();
|
|
827
|
+
const viewedRef = useRef(/* @__PURE__ */ new Set());
|
|
828
|
+
useEffect(() => {
|
|
829
|
+
if (isOpen && trackView && currentAnnouncement && !currentAnnouncement.viewed && !viewedRef.current.has(currentAnnouncement.id)) {
|
|
830
|
+
viewedRef.current.add(currentAnnouncement.id);
|
|
831
|
+
markAsViewed(currentAnnouncement.id);
|
|
832
|
+
}
|
|
833
|
+
}, [isOpen, trackView, currentAnnouncement, markAsViewed]);
|
|
834
|
+
if (!isOpen) {
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
return /* @__PURE__ */ jsx("div", { role: "dialog", "aria-modal": "true", ...props, children });
|
|
838
|
+
}
|
|
839
|
+
function AnnouncementNavigation({
|
|
840
|
+
children
|
|
841
|
+
}) {
|
|
842
|
+
const {
|
|
843
|
+
currentSlideIndex,
|
|
844
|
+
totalSlides,
|
|
845
|
+
nextSlide,
|
|
846
|
+
prevSlide,
|
|
847
|
+
setCurrentSlideIndex
|
|
848
|
+
} = useAnnouncementContext();
|
|
849
|
+
const canGoNext = currentSlideIndex < totalSlides - 1;
|
|
850
|
+
const canGoPrev = currentSlideIndex > 0;
|
|
851
|
+
return /* @__PURE__ */ jsx(Fragment, { children: children({
|
|
852
|
+
currentSlide: currentSlideIndex,
|
|
853
|
+
totalSlides,
|
|
854
|
+
canGoNext,
|
|
855
|
+
canGoPrev,
|
|
856
|
+
nextSlide,
|
|
857
|
+
prevSlide,
|
|
858
|
+
goToSlide: setCurrentSlideIndex
|
|
859
|
+
}) });
|
|
860
|
+
}
|
|
861
|
+
function AnnouncementPortal({
|
|
862
|
+
children,
|
|
863
|
+
container
|
|
864
|
+
}) {
|
|
865
|
+
const [mounted, setMounted] = useState(false);
|
|
866
|
+
useEffect(() => {
|
|
867
|
+
setMounted(true);
|
|
868
|
+
}, []);
|
|
869
|
+
if (!mounted) {
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
const target = container ?? document.body;
|
|
873
|
+
return createPortal(children, target);
|
|
874
|
+
}
|
|
875
|
+
function LauncherBadge({
|
|
876
|
+
count,
|
|
877
|
+
children,
|
|
878
|
+
max = 99
|
|
879
|
+
}) {
|
|
880
|
+
if (count <= 0) {
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
if (children) {
|
|
884
|
+
return /* @__PURE__ */ jsx(Fragment, { children: children(count) });
|
|
885
|
+
}
|
|
886
|
+
return /* @__PURE__ */ jsx("span", { children: count > max ? `${max}+` : count });
|
|
887
|
+
}
|
|
888
|
+
const LauncherContext = createContext(null);
|
|
889
|
+
function useLauncherContext() {
|
|
890
|
+
const context = useContext(LauncherContext);
|
|
891
|
+
if (!context) {
|
|
892
|
+
throw new Error(
|
|
893
|
+
"Launcher compound components must be used within LauncherRoot"
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
return context;
|
|
897
|
+
}
|
|
898
|
+
function LauncherRoot({
|
|
899
|
+
children,
|
|
900
|
+
defaultOpen = false,
|
|
901
|
+
open,
|
|
902
|
+
onOpenChange,
|
|
903
|
+
defaultActiveItem = null
|
|
904
|
+
}) {
|
|
905
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
906
|
+
const [activeItem, setActiveItem] = useState(defaultActiveItem);
|
|
907
|
+
const triggerRef = useRef(null);
|
|
908
|
+
const isOpen = open ?? internalOpen;
|
|
909
|
+
const setIsOpen = useCallback(
|
|
910
|
+
(newOpen) => {
|
|
911
|
+
if (open === void 0) {
|
|
912
|
+
setInternalOpen(newOpen);
|
|
913
|
+
}
|
|
914
|
+
if (!newOpen) {
|
|
915
|
+
setActiveItem(defaultActiveItem);
|
|
916
|
+
}
|
|
917
|
+
onOpenChange == null ? void 0 : onOpenChange(newOpen);
|
|
918
|
+
},
|
|
919
|
+
[open, onOpenChange, defaultActiveItem]
|
|
920
|
+
);
|
|
921
|
+
const toggle = useCallback(() => {
|
|
922
|
+
setIsOpen(!isOpen);
|
|
923
|
+
}, [isOpen, setIsOpen]);
|
|
924
|
+
return /* @__PURE__ */ jsx(
|
|
925
|
+
LauncherContext.Provider,
|
|
926
|
+
{
|
|
927
|
+
value: {
|
|
928
|
+
isOpen,
|
|
929
|
+
setIsOpen,
|
|
930
|
+
toggle,
|
|
931
|
+
activeItem,
|
|
932
|
+
setActiveItem,
|
|
933
|
+
triggerRef
|
|
934
|
+
},
|
|
935
|
+
children
|
|
936
|
+
}
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
function LauncherPanel({ children, ...props }) {
|
|
940
|
+
const { isOpen, setIsOpen, triggerRef } = useLauncherContext();
|
|
941
|
+
const panelRef = useRef(null);
|
|
942
|
+
useEffect(() => {
|
|
943
|
+
if (!isOpen) return;
|
|
944
|
+
const handleClickOutside = (e) => {
|
|
945
|
+
const target = e.target;
|
|
946
|
+
if (panelRef.current && !panelRef.current.contains(target) && triggerRef.current && !triggerRef.current.contains(target)) {
|
|
947
|
+
setIsOpen(false);
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
const handleEscape = (e) => {
|
|
951
|
+
if (e.key === "Escape") {
|
|
952
|
+
setIsOpen(false);
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
956
|
+
document.addEventListener("keydown", handleEscape);
|
|
957
|
+
return () => {
|
|
958
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
959
|
+
document.removeEventListener("keydown", handleEscape);
|
|
960
|
+
};
|
|
961
|
+
}, [isOpen, setIsOpen, triggerRef]);
|
|
962
|
+
if (!isOpen) {
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
return /* @__PURE__ */ jsx("div", { ref: panelRef, role: "dialog", "aria-label": "Tracey widget", ...props, children });
|
|
966
|
+
}
|
|
967
|
+
function LauncherPortal({ children, container }) {
|
|
968
|
+
const [mounted, setMounted] = useState(false);
|
|
969
|
+
useEffect(() => {
|
|
970
|
+
setMounted(true);
|
|
971
|
+
}, []);
|
|
972
|
+
if (!mounted) {
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
const target = container ?? document.body;
|
|
976
|
+
return createPortal(children, target);
|
|
977
|
+
}
|
|
978
|
+
function LauncherTab({ value, children, ...props }) {
|
|
979
|
+
const { activeItem, setActiveItem } = useLauncherContext();
|
|
980
|
+
const isActive = activeItem === value;
|
|
981
|
+
return /* @__PURE__ */ jsx(
|
|
982
|
+
"button",
|
|
983
|
+
{
|
|
984
|
+
type: "button",
|
|
985
|
+
role: "tab",
|
|
986
|
+
"aria-selected": isActive,
|
|
987
|
+
"data-state": isActive ? "active" : "inactive",
|
|
988
|
+
onClick: () => setActiveItem(value),
|
|
989
|
+
...props,
|
|
990
|
+
children
|
|
991
|
+
}
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
function LauncherTabContent({
|
|
995
|
+
value,
|
|
996
|
+
children,
|
|
997
|
+
...props
|
|
998
|
+
}) {
|
|
999
|
+
const { activeItem } = useLauncherContext();
|
|
1000
|
+
if (activeItem !== value) {
|
|
1001
|
+
return null;
|
|
1002
|
+
}
|
|
1003
|
+
return /* @__PURE__ */ jsx("div", { role: "tabpanel", ...props, children });
|
|
1004
|
+
}
|
|
1005
|
+
function LauncherTrigger({
|
|
1006
|
+
children,
|
|
1007
|
+
asChild = true
|
|
1008
|
+
}) {
|
|
1009
|
+
const { toggle, triggerRef } = useLauncherContext();
|
|
1010
|
+
const refCallback = useCallback(
|
|
1011
|
+
(node) => {
|
|
1012
|
+
triggerRef.current = node;
|
|
1013
|
+
},
|
|
1014
|
+
[triggerRef]
|
|
1015
|
+
);
|
|
1016
|
+
if (asChild) {
|
|
1017
|
+
return cloneElement(children, {
|
|
1018
|
+
ref: refCallback,
|
|
1019
|
+
onClick: (e) => {
|
|
1020
|
+
var _a, _b;
|
|
1021
|
+
(_b = (_a = children.props).onClick) == null ? void 0 : _b.call(_a, e);
|
|
1022
|
+
toggle();
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
return /* @__PURE__ */ jsx("button", { type: "button", ref: refCallback, onClick: toggle, children });
|
|
1027
|
+
}
|
|
1028
|
+
export {
|
|
1029
|
+
AnnouncementRoot as A,
|
|
1030
|
+
FeedbackFormPrimitive as F,
|
|
1031
|
+
LauncherRoot as L,
|
|
1032
|
+
MockTraceyProvider as M,
|
|
1033
|
+
TraceyProvider as T,
|
|
1034
|
+
FeedbackClose as a,
|
|
1035
|
+
FeedbackRoot as b,
|
|
1036
|
+
FeedbackTrigger as c,
|
|
1037
|
+
FeedbackPortal as d,
|
|
1038
|
+
FeedbackContent as e,
|
|
1039
|
+
useAnnouncements as f,
|
|
1040
|
+
AnnouncementPortal as g,
|
|
1041
|
+
AnnouncementContent as h,
|
|
1042
|
+
AnnouncementNavigation as i,
|
|
1043
|
+
AnnouncementClose as j,
|
|
1044
|
+
LauncherTrigger as k,
|
|
1045
|
+
LauncherBadge as l,
|
|
1046
|
+
LauncherPortal as m,
|
|
1047
|
+
LauncherPanel as n,
|
|
1048
|
+
LauncherTab as o,
|
|
1049
|
+
LauncherTabContent as p,
|
|
1050
|
+
useFeedback as q,
|
|
1051
|
+
useTraceyEvents as r,
|
|
1052
|
+
useAnnouncementContext as s,
|
|
1053
|
+
useLauncherContext as t,
|
|
1054
|
+
useTracey as u
|
|
1055
|
+
};
|
|
1056
|
+
//# sourceMappingURL=LauncherTrigger-5pNAvA05.js.map
|