@feedhog/js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,215 @@
1
+ # @feedhog/js
2
+
3
+ JavaScript SDK for [Feedhog](https://feedhog.com) - collect user feedback from any website or application.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @feedhog/js
9
+ # or
10
+ pnpm add @feedhog/js
11
+ # or
12
+ yarn add @feedhog/js
13
+ ```
14
+
15
+ ### CDN Usage
16
+
17
+ ```html
18
+ <script src="https://feedhog.com/sdk.js"></script>
19
+ <script>
20
+ const feedhog = new Feedhog({ apiKey: 'fhpk_xxx' });
21
+ </script>
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```typescript
27
+ import Feedhog from '@feedhog/js';
28
+
29
+ // Initialize with your public API key
30
+ const feedhog = new Feedhog({
31
+ apiKey: 'fhpk_xxx',
32
+ baseUrl: 'https://feedhog.com' // Optional, defaults to production
33
+ });
34
+
35
+ // Identify the current user (optional but recommended)
36
+ await feedhog.identify({
37
+ externalId: 'user-123',
38
+ email: 'user@example.com',
39
+ name: 'John Doe',
40
+ metadata: { plan: 'pro' }
41
+ });
42
+
43
+ // Submit feedback
44
+ const feedback = await feedhog.submit({
45
+ title: 'Add dark mode',
46
+ description: 'Would love a dark theme option',
47
+ type: 'idea' // 'bug' | 'idea' | 'question' | 'other'
48
+ });
49
+ ```
50
+
51
+ ## API Reference
52
+
53
+ ### `new Feedhog(config)`
54
+
55
+ Create a new Feedhog instance.
56
+
57
+ ```typescript
58
+ const feedhog = new Feedhog({
59
+ apiKey: 'fhpk_xxx', // Required: Your public API key
60
+ baseUrl: 'https://...' // Optional: Custom API URL
61
+ });
62
+ ```
63
+
64
+ ### `feedhog.identify(user)`
65
+
66
+ Identify the current user. This associates all subsequent feedback and votes with this user.
67
+
68
+ ```typescript
69
+ await feedhog.identify({
70
+ externalId: 'user-123', // Required: Your system's user ID
71
+ email: 'user@example.com', // Optional
72
+ name: 'John Doe', // Optional
73
+ avatarUrl: 'https://...', // Optional
74
+ metadata: { plan: 'pro' } // Optional: Custom data
75
+ });
76
+ ```
77
+
78
+ User identity is persisted in localStorage, so returning users are automatically recognized.
79
+
80
+ ### `feedhog.submit(input)`
81
+
82
+ Submit new feedback.
83
+
84
+ ```typescript
85
+ const feedback = await feedhog.submit({
86
+ title: 'Feature request', // Required
87
+ description: 'Detailed description', // Optional
88
+ type: 'idea', // Optional: 'bug' | 'idea' | 'question' | 'other'
89
+ metadata: { page: '/settings' } // Optional: Custom data
90
+ });
91
+ ```
92
+
93
+ ### `feedhog.list(options)`
94
+
95
+ List feedback items with optional filters.
96
+
97
+ ```typescript
98
+ const { items, total, page, totalPages } = await feedhog.list({
99
+ status: ['new', 'planned'], // Filter by status(es)
100
+ type: 'idea', // Filter by type
101
+ search: 'dark mode', // Search query
102
+ sortBy: 'votes', // 'newest' | 'oldest' | 'votes' | 'comments'
103
+ page: 1, // Page number
104
+ limit: 20 // Items per page (max 100)
105
+ });
106
+ ```
107
+
108
+ ### `feedhog.get(feedbackId)`
109
+
110
+ Get a single feedback item with full details including comments.
111
+
112
+ ```typescript
113
+ const feedback = await feedhog.get('abc123');
114
+ console.log(feedback.comments);
115
+ console.log(feedback.userHasVoted);
116
+ ```
117
+
118
+ ### `feedhog.vote(feedbackId)`
119
+
120
+ Toggle vote on a feedback item.
121
+
122
+ ```typescript
123
+ const { voted, voteCount } = await feedhog.vote('abc123');
124
+ // voted: true if vote was added, false if removed
125
+ ```
126
+
127
+ ### `feedhog.hasVoted(feedbackId)`
128
+
129
+ Check if the current user has voted on a feedback item.
130
+
131
+ ```typescript
132
+ const { voted, voteCount } = await feedhog.hasVoted('abc123');
133
+ ```
134
+
135
+ ### `feedhog.reset()`
136
+
137
+ Clear the current user and stored data. Use when a user logs out.
138
+
139
+ ```typescript
140
+ feedhog.reset();
141
+ ```
142
+
143
+ ### `feedhog.user`
144
+
145
+ Get the currently identified user.
146
+
147
+ ```typescript
148
+ if (feedhog.user) {
149
+ console.log('Logged in as:', feedhog.user.name);
150
+ }
151
+ ```
152
+
153
+ ## Events
154
+
155
+ Subscribe to SDK events:
156
+
157
+ ```typescript
158
+ // User identified
159
+ feedhog.on('identify', (user) => {
160
+ console.log('User identified:', user);
161
+ });
162
+
163
+ // Feedback submitted
164
+ feedhog.on('submit', (feedback) => {
165
+ console.log('Feedback submitted:', feedback.id);
166
+ });
167
+
168
+ // Vote toggled
169
+ feedhog.on('vote', ({ feedbackId, result }) => {
170
+ console.log('Vote toggled:', result.voted);
171
+ });
172
+
173
+ // Error occurred
174
+ feedhog.on('error', (error) => {
175
+ console.error('SDK error:', error);
176
+ });
177
+
178
+ // User reset
179
+ feedhog.on('reset', () => {
180
+ console.log('User logged out');
181
+ });
182
+ ```
183
+
184
+ ## TypeScript
185
+
186
+ The SDK is fully typed. Import types as needed:
187
+
188
+ ```typescript
189
+ import Feedhog, {
190
+ type FeedbackType,
191
+ type FeedbackStatus,
192
+ type FeedbackListItem,
193
+ type UserIdentity
194
+ } from '@feedhog/js';
195
+ ```
196
+
197
+ ## Error Handling
198
+
199
+ ```typescript
200
+ import Feedhog, { FeedhogApiError } from '@feedhog/js';
201
+
202
+ try {
203
+ await feedhog.submit({ title: '' });
204
+ } catch (error) {
205
+ if (error instanceof FeedhogApiError) {
206
+ console.log('Status:', error.status);
207
+ console.log('Message:', error.message);
208
+ console.log('Field errors:', error.details?.fieldErrors);
209
+ }
210
+ }
211
+ ```
212
+
213
+ ## License
214
+
215
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,426 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Feedhog: () => Feedhog,
24
+ FeedhogApiError: () => FeedhogApiError,
25
+ default: () => index_default
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/utils.ts
30
+ var STORAGE_KEY = "feedhog_user";
31
+ function storeUser(user) {
32
+ if (typeof window === "undefined" || !window.localStorage) return;
33
+ try {
34
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(user));
35
+ } catch {
36
+ }
37
+ }
38
+ function getStoredUser() {
39
+ if (typeof window === "undefined" || !window.localStorage) return null;
40
+ try {
41
+ const stored = localStorage.getItem(STORAGE_KEY);
42
+ if (!stored) return null;
43
+ return JSON.parse(stored);
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ function clearStoredUser() {
49
+ if (typeof window === "undefined" || !window.localStorage) return;
50
+ try {
51
+ localStorage.removeItem(STORAGE_KEY);
52
+ } catch {
53
+ }
54
+ }
55
+ function buildUrl(baseUrl, path, params) {
56
+ const url = new URL(path, baseUrl);
57
+ if (params) {
58
+ for (const [key, value] of Object.entries(params)) {
59
+ if (value === void 0) continue;
60
+ if (Array.isArray(value)) {
61
+ url.searchParams.set(key, value.join(","));
62
+ } else {
63
+ url.searchParams.set(key, String(value));
64
+ }
65
+ }
66
+ }
67
+ return url.toString();
68
+ }
69
+ function isBrowser() {
70
+ return typeof window !== "undefined" && typeof document !== "undefined";
71
+ }
72
+ var EventEmitter = class {
73
+ constructor() {
74
+ this.listeners = /* @__PURE__ */ new Map();
75
+ }
76
+ on(event, callback) {
77
+ if (!this.listeners.has(event)) {
78
+ this.listeners.set(event, /* @__PURE__ */ new Set());
79
+ }
80
+ this.listeners.get(event).add(callback);
81
+ return () => {
82
+ this.listeners.get(event)?.delete(callback);
83
+ };
84
+ }
85
+ emit(event, data) {
86
+ this.listeners.get(event)?.forEach((callback) => callback(data));
87
+ }
88
+ off(event, callback) {
89
+ this.listeners.get(event)?.delete(callback);
90
+ }
91
+ };
92
+
93
+ // src/client.ts
94
+ var DEFAULT_BASE_URL = "https://feedhog.com";
95
+ var FeedhogClient = class {
96
+ constructor(config) {
97
+ if (!config.apiKey) {
98
+ throw new Error("Feedhog: apiKey is required");
99
+ }
100
+ this.apiKey = config.apiKey;
101
+ this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
102
+ }
103
+ /**
104
+ * Make an authenticated API request
105
+ */
106
+ async request(method, path, options) {
107
+ const url = options?.params ? buildUrl(this.baseUrl, path, options.params) : `${this.baseUrl}${path}`;
108
+ const response = await fetch(url, {
109
+ method,
110
+ headers: {
111
+ "Content-Type": "application/json",
112
+ "x-api-key": this.apiKey
113
+ },
114
+ body: options?.body ? JSON.stringify(options.body) : void 0
115
+ });
116
+ const data = await response.json();
117
+ if (!response.ok) {
118
+ const error = data;
119
+ throw new FeedhogApiError(
120
+ error.error || "Unknown error",
121
+ response.status,
122
+ error.details
123
+ );
124
+ }
125
+ return data;
126
+ }
127
+ /**
128
+ * Identify or create an end user
129
+ */
130
+ async identify(user) {
131
+ const response = await this.request(
132
+ "POST",
133
+ "/api/v1/identify",
134
+ { body: user }
135
+ );
136
+ return response.user;
137
+ }
138
+ /**
139
+ * Submit new feedback
140
+ */
141
+ async submit(input, user) {
142
+ const body = {
143
+ ...input
144
+ };
145
+ if (user) {
146
+ body.endUser = user;
147
+ }
148
+ const response = await this.request(
149
+ "POST",
150
+ "/api/v1/feedback",
151
+ { body }
152
+ );
153
+ return response.feedback;
154
+ }
155
+ /**
156
+ * List feedback items (paginated)
157
+ */
158
+ async list(options) {
159
+ return this.request(
160
+ "GET",
161
+ "/api/v1/feedback",
162
+ {
163
+ params: {
164
+ status: options?.status ? Array.isArray(options.status) ? options.status : [options.status] : void 0,
165
+ type: options?.type ? Array.isArray(options.type) ? options.type : [options.type] : void 0,
166
+ search: options?.search,
167
+ sortBy: options?.sortBy,
168
+ page: options?.page,
169
+ limit: options?.limit
170
+ }
171
+ }
172
+ );
173
+ }
174
+ /**
175
+ * Get a single feedback item with details
176
+ */
177
+ async get(feedbackId, endUserId) {
178
+ const response = await this.request(
179
+ "GET",
180
+ `/api/v1/feedback/${feedbackId}`,
181
+ {
182
+ params: endUserId ? { endUserId } : void 0
183
+ }
184
+ );
185
+ return response.feedback;
186
+ }
187
+ /**
188
+ * Toggle vote on feedback
189
+ */
190
+ async vote(feedbackId, user) {
191
+ return this.request("POST", `/api/v1/feedback/${feedbackId}/vote`, {
192
+ body: user ? { endUser: user } : {}
193
+ });
194
+ }
195
+ /**
196
+ * Check if user has voted on feedback
197
+ */
198
+ async hasVoted(feedbackId, endUserId) {
199
+ return this.request("GET", `/api/v1/feedback/${feedbackId}/vote`, {
200
+ params: endUserId ? { endUserId } : void 0
201
+ });
202
+ }
203
+ };
204
+ var FeedhogApiError = class extends Error {
205
+ constructor(message, status, details) {
206
+ super(message);
207
+ this.status = status;
208
+ this.details = details;
209
+ this.name = "FeedhogApiError";
210
+ }
211
+ };
212
+
213
+ // src/index.ts
214
+ var Feedhog = class extends EventEmitter {
215
+ constructor(config) {
216
+ super();
217
+ this.currentUser = null;
218
+ this.client = new FeedhogClient(config);
219
+ if (isBrowser()) {
220
+ const stored = getStoredUser();
221
+ if (stored) {
222
+ this.currentUser = { ...stored, identified: false };
223
+ }
224
+ }
225
+ }
226
+ /**
227
+ * Get the currently identified user
228
+ */
229
+ get user() {
230
+ return this.currentUser;
231
+ }
232
+ /**
233
+ * Identify the current user
234
+ *
235
+ * This associates feedback and votes with a specific user in your system.
236
+ * User data is persisted in localStorage for subsequent sessions.
237
+ *
238
+ * @param user - User identity data
239
+ * @returns Promise resolving to the identified user
240
+ *
241
+ * @example
242
+ * ```typescript
243
+ * await feedhog.identify({
244
+ * externalId: 'user-123',
245
+ * email: 'user@example.com',
246
+ * name: 'John Doe',
247
+ * metadata: { plan: 'pro' }
248
+ * });
249
+ * ```
250
+ */
251
+ async identify(user) {
252
+ try {
253
+ const identified = await this.client.identify(user);
254
+ this.currentUser = {
255
+ ...user,
256
+ identified: true
257
+ };
258
+ if (isBrowser()) {
259
+ storeUser(user);
260
+ }
261
+ this.emit("identify", identified);
262
+ return identified;
263
+ } catch (error) {
264
+ this.emit("error", error);
265
+ throw error;
266
+ }
267
+ }
268
+ /**
269
+ * Submit new feedback
270
+ *
271
+ * If a user has been identified, the feedback will be associated with them.
272
+ *
273
+ * @param input - Feedback data
274
+ * @returns Promise resolving to the created feedback item
275
+ *
276
+ * @example
277
+ * ```typescript
278
+ * const feedback = await feedhog.submit({
279
+ * title: 'Add dark mode',
280
+ * description: 'Would love a dark theme option',
281
+ * type: 'idea'
282
+ * });
283
+ * console.log('Feedback submitted:', feedback.id);
284
+ * ```
285
+ */
286
+ async submit(input) {
287
+ try {
288
+ const feedback = await this.client.submit(
289
+ input,
290
+ this.currentUser || void 0
291
+ );
292
+ this.emit("submit", feedback);
293
+ return feedback;
294
+ } catch (error) {
295
+ this.emit("error", error);
296
+ throw error;
297
+ }
298
+ }
299
+ /**
300
+ * List feedback items
301
+ *
302
+ * Returns a paginated list of feedback for the project.
303
+ *
304
+ * @param options - Filter and pagination options
305
+ * @returns Promise resolving to paginated feedback list
306
+ *
307
+ * @example
308
+ * ```typescript
309
+ * // Get all ideas in "planned" status
310
+ * const { items, total } = await feedhog.list({
311
+ * status: ['planned', 'in-progress'],
312
+ * type: 'idea',
313
+ * sortBy: 'votes',
314
+ * limit: 10
315
+ * });
316
+ * ```
317
+ */
318
+ async list(options) {
319
+ try {
320
+ return await this.client.list(options);
321
+ } catch (error) {
322
+ this.emit("error", error);
323
+ throw error;
324
+ }
325
+ }
326
+ /**
327
+ * Get a single feedback item with full details
328
+ *
329
+ * @param feedbackId - ID of the feedback item
330
+ * @returns Promise resolving to feedback details including comments
331
+ *
332
+ * @example
333
+ * ```typescript
334
+ * const feedback = await feedhog.get('abc123');
335
+ * console.log('Comments:', feedback.comments.length);
336
+ * console.log('Has voted:', feedback.userHasVoted);
337
+ * ```
338
+ */
339
+ async get(feedbackId) {
340
+ try {
341
+ return await this.client.get(
342
+ feedbackId,
343
+ this.currentUser?.externalId
344
+ );
345
+ } catch (error) {
346
+ this.emit("error", error);
347
+ throw error;
348
+ }
349
+ }
350
+ /**
351
+ * Toggle vote on a feedback item
352
+ *
353
+ * If the user has already voted, this will remove their vote.
354
+ * If they haven't voted, it will add their vote.
355
+ *
356
+ * @param feedbackId - ID of the feedback item to vote on
357
+ * @returns Promise resolving to vote result
358
+ *
359
+ * @example
360
+ * ```typescript
361
+ * const { voted, voteCount } = await feedhog.vote('abc123');
362
+ * console.log(voted ? 'Vote added' : 'Vote removed');
363
+ * console.log('Total votes:', voteCount);
364
+ * ```
365
+ */
366
+ async vote(feedbackId) {
367
+ try {
368
+ const result = await this.client.vote(
369
+ feedbackId,
370
+ this.currentUser || void 0
371
+ );
372
+ this.emit("vote", { feedbackId, result });
373
+ return result;
374
+ } catch (error) {
375
+ this.emit("error", error);
376
+ throw error;
377
+ }
378
+ }
379
+ /**
380
+ * Check if the current user has voted on a feedback item
381
+ *
382
+ * @param feedbackId - ID of the feedback item
383
+ * @returns Promise resolving to vote status
384
+ *
385
+ * @example
386
+ * ```typescript
387
+ * const { voted, voteCount } = await feedhog.hasVoted('abc123');
388
+ * ```
389
+ */
390
+ async hasVoted(feedbackId) {
391
+ try {
392
+ return await this.client.hasVoted(
393
+ feedbackId,
394
+ this.currentUser?.externalId
395
+ );
396
+ } catch (error) {
397
+ this.emit("error", error);
398
+ throw error;
399
+ }
400
+ }
401
+ /**
402
+ * Reset the SDK state
403
+ *
404
+ * Clears the current user and removes stored data.
405
+ * Use this when a user logs out.
406
+ *
407
+ * @example
408
+ * ```typescript
409
+ * feedhog.reset();
410
+ * ```
411
+ */
412
+ reset() {
413
+ this.currentUser = null;
414
+ if (isBrowser()) {
415
+ clearStoredUser();
416
+ }
417
+ this.emit("reset", void 0);
418
+ }
419
+ };
420
+ var index_default = Feedhog;
421
+ // Annotate the CommonJS export names for ESM import in node:
422
+ 0 && (module.exports = {
423
+ Feedhog,
424
+ FeedhogApiError
425
+ });
426
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/utils.ts","../src/client.ts"],"sourcesContent":["/**\n * Feedhog JavaScript SDK\n *\n * Collect user feedback from any website or application.\n *\n * @example\n * ```typescript\n * import Feedhog from '@feedhog/js';\n *\n * const feedhog = new Feedhog({ apiKey: 'fhpk_xxx' });\n *\n * // Identify the current user (optional but recommended)\n * feedhog.identify({\n * externalId: 'user-123',\n * email: 'user@example.com',\n * name: 'John Doe'\n * });\n *\n * // Submit feedback\n * await feedhog.submit({\n * title: 'Add dark mode',\n * description: 'Would love a dark theme option',\n * type: 'idea'\n * });\n * ```\n */\n\nimport { FeedhogClient, FeedhogApiError } from \"./client\";\nimport {\n storeUser,\n getStoredUser,\n clearStoredUser,\n EventEmitter,\n isBrowser,\n} from \"./utils\";\nimport type {\n FeedhogConfig,\n UserIdentity,\n SubmitFeedbackInput,\n ListFeedbackOptions,\n FeedbackListItem,\n FeedbackDetail,\n PaginatedResponse,\n VoteResult,\n IdentifiedUser,\n CurrentUser,\n FeedbackType,\n FeedbackStatus,\n SortBy,\n} from \"./types\";\n\n// SDK Events\ninterface FeedhogEvents {\n [key: string]: unknown;\n identify: IdentifiedUser;\n submit: FeedbackListItem;\n vote: { feedbackId: string; result: VoteResult };\n error: Error;\n reset: void;\n}\n\n/**\n * Feedhog SDK main class\n */\nexport class Feedhog extends EventEmitter<FeedhogEvents> {\n private readonly client: FeedhogClient;\n private currentUser: CurrentUser | null = null;\n\n constructor(config: FeedhogConfig) {\n super();\n this.client = new FeedhogClient(config);\n\n // Try to restore user from storage\n if (isBrowser()) {\n const stored = getStoredUser();\n if (stored) {\n this.currentUser = { ...stored, identified: false };\n }\n }\n }\n\n /**\n * Get the currently identified user\n */\n get user(): CurrentUser | null {\n return this.currentUser;\n }\n\n /**\n * Identify the current user\n *\n * This associates feedback and votes with a specific user in your system.\n * User data is persisted in localStorage for subsequent sessions.\n *\n * @param user - User identity data\n * @returns Promise resolving to the identified user\n *\n * @example\n * ```typescript\n * await feedhog.identify({\n * externalId: 'user-123',\n * email: 'user@example.com',\n * name: 'John Doe',\n * metadata: { plan: 'pro' }\n * });\n * ```\n */\n async identify(user: UserIdentity): Promise<IdentifiedUser> {\n try {\n const identified = await this.client.identify(user);\n\n this.currentUser = {\n ...user,\n identified: true,\n };\n\n // Persist to storage\n if (isBrowser()) {\n storeUser(user);\n }\n\n this.emit(\"identify\", identified);\n return identified;\n } catch (error) {\n this.emit(\"error\", error as Error);\n throw error;\n }\n }\n\n /**\n * Submit new feedback\n *\n * If a user has been identified, the feedback will be associated with them.\n *\n * @param input - Feedback data\n * @returns Promise resolving to the created feedback item\n *\n * @example\n * ```typescript\n * const feedback = await feedhog.submit({\n * title: 'Add dark mode',\n * description: 'Would love a dark theme option',\n * type: 'idea'\n * });\n * console.log('Feedback submitted:', feedback.id);\n * ```\n */\n async submit(input: SubmitFeedbackInput): Promise<FeedbackListItem> {\n try {\n const feedback = await this.client.submit(\n input,\n this.currentUser || undefined\n );\n this.emit(\"submit\", feedback);\n return feedback;\n } catch (error) {\n this.emit(\"error\", error as Error);\n throw error;\n }\n }\n\n /**\n * List feedback items\n *\n * Returns a paginated list of feedback for the project.\n *\n * @param options - Filter and pagination options\n * @returns Promise resolving to paginated feedback list\n *\n * @example\n * ```typescript\n * // Get all ideas in \"planned\" status\n * const { items, total } = await feedhog.list({\n * status: ['planned', 'in-progress'],\n * type: 'idea',\n * sortBy: 'votes',\n * limit: 10\n * });\n * ```\n */\n async list(\n options?: ListFeedbackOptions\n ): Promise<PaginatedResponse<FeedbackListItem>> {\n try {\n return await this.client.list(options);\n } catch (error) {\n this.emit(\"error\", error as Error);\n throw error;\n }\n }\n\n /**\n * Get a single feedback item with full details\n *\n * @param feedbackId - ID of the feedback item\n * @returns Promise resolving to feedback details including comments\n *\n * @example\n * ```typescript\n * const feedback = await feedhog.get('abc123');\n * console.log('Comments:', feedback.comments.length);\n * console.log('Has voted:', feedback.userHasVoted);\n * ```\n */\n async get(feedbackId: string): Promise<FeedbackDetail> {\n try {\n return await this.client.get(\n feedbackId,\n this.currentUser?.externalId\n );\n } catch (error) {\n this.emit(\"error\", error as Error);\n throw error;\n }\n }\n\n /**\n * Toggle vote on a feedback item\n *\n * If the user has already voted, this will remove their vote.\n * If they haven't voted, it will add their vote.\n *\n * @param feedbackId - ID of the feedback item to vote on\n * @returns Promise resolving to vote result\n *\n * @example\n * ```typescript\n * const { voted, voteCount } = await feedhog.vote('abc123');\n * console.log(voted ? 'Vote added' : 'Vote removed');\n * console.log('Total votes:', voteCount);\n * ```\n */\n async vote(feedbackId: string): Promise<VoteResult> {\n try {\n const result = await this.client.vote(\n feedbackId,\n this.currentUser || undefined\n );\n this.emit(\"vote\", { feedbackId, result });\n return result;\n } catch (error) {\n this.emit(\"error\", error as Error);\n throw error;\n }\n }\n\n /**\n * Check if the current user has voted on a feedback item\n *\n * @param feedbackId - ID of the feedback item\n * @returns Promise resolving to vote status\n *\n * @example\n * ```typescript\n * const { voted, voteCount } = await feedhog.hasVoted('abc123');\n * ```\n */\n async hasVoted(feedbackId: string): Promise<VoteResult> {\n try {\n return await this.client.hasVoted(\n feedbackId,\n this.currentUser?.externalId\n );\n } catch (error) {\n this.emit(\"error\", error as Error);\n throw error;\n }\n }\n\n /**\n * Reset the SDK state\n *\n * Clears the current user and removes stored data.\n * Use this when a user logs out.\n *\n * @example\n * ```typescript\n * feedhog.reset();\n * ```\n */\n reset(): void {\n this.currentUser = null;\n if (isBrowser()) {\n clearStoredUser();\n }\n this.emit(\"reset\", undefined);\n }\n}\n\n// Export as default for UMD builds\nexport default Feedhog;\n\n// Export types\nexport type {\n FeedhogConfig,\n UserIdentity,\n SubmitFeedbackInput,\n ListFeedbackOptions,\n FeedbackListItem,\n FeedbackDetail,\n PaginatedResponse,\n VoteResult,\n IdentifiedUser,\n CurrentUser,\n FeedbackType,\n FeedbackStatus,\n SortBy,\n};\n\n// Export error class\nexport { FeedhogApiError };\n","/**\n * Feedhog SDK Utilities\n */\n\nconst STORAGE_KEY = \"feedhog_user\";\n\n/**\n * Store user identity in localStorage (if available)\n */\nexport function storeUser(user: {\n externalId: string;\n email?: string;\n name?: string;\n avatarUrl?: string;\n}): void {\n if (typeof window === \"undefined\" || !window.localStorage) return;\n\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(user));\n } catch {\n // Ignore storage errors (quota exceeded, private mode, etc.)\n }\n}\n\n/**\n * Retrieve stored user identity from localStorage\n */\nexport function getStoredUser(): {\n externalId: string;\n email?: string;\n name?: string;\n avatarUrl?: string;\n} | null {\n if (typeof window === \"undefined\" || !window.localStorage) return null;\n\n try {\n const stored = localStorage.getItem(STORAGE_KEY);\n if (!stored) return null;\n return JSON.parse(stored);\n } catch {\n return null;\n }\n}\n\n/**\n * Clear stored user identity\n */\nexport function clearStoredUser(): void {\n if (typeof window === \"undefined\" || !window.localStorage) return;\n\n try {\n localStorage.removeItem(STORAGE_KEY);\n } catch {\n // Ignore storage errors\n }\n}\n\n/**\n * Build URL with query parameters\n */\nexport function buildUrl(\n baseUrl: string,\n path: string,\n params?: Record<string, string | string[] | number | undefined>\n): string {\n const url = new URL(path, baseUrl);\n\n if (params) {\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined) continue;\n\n if (Array.isArray(value)) {\n url.searchParams.set(key, value.join(\",\"));\n } else {\n url.searchParams.set(key, String(value));\n }\n }\n }\n\n return url.toString();\n}\n\n/**\n * Check if we're running in a browser environment\n */\nexport function isBrowser(): boolean {\n return typeof window !== \"undefined\" && typeof document !== \"undefined\";\n}\n\n/**\n * Simple event emitter for SDK events\n */\nexport class EventEmitter<Events extends { [key: string]: unknown }> {\n private listeners: Map<keyof Events, Set<(data: unknown) => void>> =\n new Map();\n\n on<K extends keyof Events>(\n event: K,\n callback: (data: Events[K]) => void\n ): () => void {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n this.listeners.get(event)!.add(callback as (data: unknown) => void);\n\n // Return unsubscribe function\n return () => {\n this.listeners.get(event)?.delete(callback as (data: unknown) => void);\n };\n }\n\n emit<K extends keyof Events>(event: K, data: Events[K]): void {\n this.listeners.get(event)?.forEach((callback) => callback(data));\n }\n\n off<K extends keyof Events>(\n event: K,\n callback: (data: Events[K]) => void\n ): void {\n this.listeners.get(event)?.delete(callback as (data: unknown) => void);\n }\n}\n","/**\n * Feedhog API Client\n * Handles all HTTP communication with the Feedhog API.\n */\n\nimport type {\n FeedhogConfig,\n UserIdentity,\n SubmitFeedbackInput,\n ListFeedbackOptions,\n FeedbackListItem,\n FeedbackDetail,\n PaginatedResponse,\n VoteResult,\n IdentifiedUser,\n ApiError,\n} from \"./types\";\nimport { buildUrl } from \"./utils\";\n\nconst DEFAULT_BASE_URL = \"https://feedhog.com\";\n\nexport class FeedhogClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n\n constructor(config: FeedhogConfig) {\n if (!config.apiKey) {\n throw new Error(\"Feedhog: apiKey is required\");\n }\n\n this.apiKey = config.apiKey;\n this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\\/$/, \"\");\n }\n\n /**\n * Make an authenticated API request\n */\n private async request<T>(\n method: string,\n path: string,\n options?: {\n body?: unknown;\n params?: Record<string, string | string[] | number | undefined>;\n }\n ): Promise<T> {\n const url = options?.params\n ? buildUrl(this.baseUrl, path, options.params)\n : `${this.baseUrl}${path}`;\n\n const response = await fetch(url, {\n method,\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": this.apiKey,\n },\n body: options?.body ? JSON.stringify(options.body) : undefined,\n });\n\n const data = await response.json();\n\n if (!response.ok) {\n const error = data as ApiError;\n throw new FeedhogApiError(\n error.error || \"Unknown error\",\n response.status,\n error.details\n );\n }\n\n return data as T;\n }\n\n /**\n * Identify or create an end user\n */\n async identify(user: UserIdentity): Promise<IdentifiedUser> {\n const response = await this.request<{ user: IdentifiedUser }>(\n \"POST\",\n \"/api/v1/identify\",\n { body: user }\n );\n return response.user;\n }\n\n /**\n * Submit new feedback\n */\n async submit(\n input: SubmitFeedbackInput,\n user?: UserIdentity\n ): Promise<FeedbackListItem> {\n const body: SubmitFeedbackInput & { endUser?: UserIdentity } = {\n ...input,\n };\n if (user) {\n body.endUser = user;\n }\n\n const response = await this.request<{ feedback: FeedbackListItem }>(\n \"POST\",\n \"/api/v1/feedback\",\n { body }\n );\n return response.feedback;\n }\n\n /**\n * List feedback items (paginated)\n */\n async list(\n options?: ListFeedbackOptions\n ): Promise<PaginatedResponse<FeedbackListItem>> {\n return this.request<PaginatedResponse<FeedbackListItem>>(\n \"GET\",\n \"/api/v1/feedback\",\n {\n params: {\n status: options?.status\n ? Array.isArray(options.status)\n ? options.status\n : [options.status]\n : undefined,\n type: options?.type\n ? Array.isArray(options.type)\n ? options.type\n : [options.type]\n : undefined,\n search: options?.search,\n sortBy: options?.sortBy,\n page: options?.page,\n limit: options?.limit,\n },\n }\n );\n }\n\n /**\n * Get a single feedback item with details\n */\n async get(feedbackId: string, endUserId?: string): Promise<FeedbackDetail> {\n const response = await this.request<{ feedback: FeedbackDetail }>(\n \"GET\",\n `/api/v1/feedback/${feedbackId}`,\n {\n params: endUserId ? { endUserId } : undefined,\n }\n );\n return response.feedback;\n }\n\n /**\n * Toggle vote on feedback\n */\n async vote(feedbackId: string, user?: UserIdentity): Promise<VoteResult> {\n return this.request<VoteResult>(\"POST\", `/api/v1/feedback/${feedbackId}/vote`, {\n body: user ? { endUser: user } : {},\n });\n }\n\n /**\n * Check if user has voted on feedback\n */\n async hasVoted(feedbackId: string, endUserId?: string): Promise<VoteResult> {\n return this.request<VoteResult>(\"GET\", `/api/v1/feedback/${feedbackId}/vote`, {\n params: endUserId ? { endUserId } : undefined,\n });\n }\n}\n\n/**\n * API Error with status code and optional validation details\n */\nexport class FeedhogApiError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly details?: {\n formErrors: string[];\n fieldErrors: Record<string, string[]>;\n }\n ) {\n super(message);\n this.name = \"FeedhogApiError\";\n }\n}\n"],"mappings":"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","names":[]}