@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/dist/index.js ADDED
@@ -0,0 +1,398 @@
1
+ // src/utils.ts
2
+ var STORAGE_KEY = "feedhog_user";
3
+ function storeUser(user) {
4
+ if (typeof window === "undefined" || !window.localStorage) return;
5
+ try {
6
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(user));
7
+ } catch {
8
+ }
9
+ }
10
+ function getStoredUser() {
11
+ if (typeof window === "undefined" || !window.localStorage) return null;
12
+ try {
13
+ const stored = localStorage.getItem(STORAGE_KEY);
14
+ if (!stored) return null;
15
+ return JSON.parse(stored);
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+ function clearStoredUser() {
21
+ if (typeof window === "undefined" || !window.localStorage) return;
22
+ try {
23
+ localStorage.removeItem(STORAGE_KEY);
24
+ } catch {
25
+ }
26
+ }
27
+ function buildUrl(baseUrl, path, params) {
28
+ const url = new URL(path, baseUrl);
29
+ if (params) {
30
+ for (const [key, value] of Object.entries(params)) {
31
+ if (value === void 0) continue;
32
+ if (Array.isArray(value)) {
33
+ url.searchParams.set(key, value.join(","));
34
+ } else {
35
+ url.searchParams.set(key, String(value));
36
+ }
37
+ }
38
+ }
39
+ return url.toString();
40
+ }
41
+ function isBrowser() {
42
+ return typeof window !== "undefined" && typeof document !== "undefined";
43
+ }
44
+ var EventEmitter = class {
45
+ constructor() {
46
+ this.listeners = /* @__PURE__ */ new Map();
47
+ }
48
+ on(event, callback) {
49
+ if (!this.listeners.has(event)) {
50
+ this.listeners.set(event, /* @__PURE__ */ new Set());
51
+ }
52
+ this.listeners.get(event).add(callback);
53
+ return () => {
54
+ this.listeners.get(event)?.delete(callback);
55
+ };
56
+ }
57
+ emit(event, data) {
58
+ this.listeners.get(event)?.forEach((callback) => callback(data));
59
+ }
60
+ off(event, callback) {
61
+ this.listeners.get(event)?.delete(callback);
62
+ }
63
+ };
64
+
65
+ // src/client.ts
66
+ var DEFAULT_BASE_URL = "https://feedhog.com";
67
+ var FeedhogClient = class {
68
+ constructor(config) {
69
+ if (!config.apiKey) {
70
+ throw new Error("Feedhog: apiKey is required");
71
+ }
72
+ this.apiKey = config.apiKey;
73
+ this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
74
+ }
75
+ /**
76
+ * Make an authenticated API request
77
+ */
78
+ async request(method, path, options) {
79
+ const url = options?.params ? buildUrl(this.baseUrl, path, options.params) : `${this.baseUrl}${path}`;
80
+ const response = await fetch(url, {
81
+ method,
82
+ headers: {
83
+ "Content-Type": "application/json",
84
+ "x-api-key": this.apiKey
85
+ },
86
+ body: options?.body ? JSON.stringify(options.body) : void 0
87
+ });
88
+ const data = await response.json();
89
+ if (!response.ok) {
90
+ const error = data;
91
+ throw new FeedhogApiError(
92
+ error.error || "Unknown error",
93
+ response.status,
94
+ error.details
95
+ );
96
+ }
97
+ return data;
98
+ }
99
+ /**
100
+ * Identify or create an end user
101
+ */
102
+ async identify(user) {
103
+ const response = await this.request(
104
+ "POST",
105
+ "/api/v1/identify",
106
+ { body: user }
107
+ );
108
+ return response.user;
109
+ }
110
+ /**
111
+ * Submit new feedback
112
+ */
113
+ async submit(input, user) {
114
+ const body = {
115
+ ...input
116
+ };
117
+ if (user) {
118
+ body.endUser = user;
119
+ }
120
+ const response = await this.request(
121
+ "POST",
122
+ "/api/v1/feedback",
123
+ { body }
124
+ );
125
+ return response.feedback;
126
+ }
127
+ /**
128
+ * List feedback items (paginated)
129
+ */
130
+ async list(options) {
131
+ return this.request(
132
+ "GET",
133
+ "/api/v1/feedback",
134
+ {
135
+ params: {
136
+ status: options?.status ? Array.isArray(options.status) ? options.status : [options.status] : void 0,
137
+ type: options?.type ? Array.isArray(options.type) ? options.type : [options.type] : void 0,
138
+ search: options?.search,
139
+ sortBy: options?.sortBy,
140
+ page: options?.page,
141
+ limit: options?.limit
142
+ }
143
+ }
144
+ );
145
+ }
146
+ /**
147
+ * Get a single feedback item with details
148
+ */
149
+ async get(feedbackId, endUserId) {
150
+ const response = await this.request(
151
+ "GET",
152
+ `/api/v1/feedback/${feedbackId}`,
153
+ {
154
+ params: endUserId ? { endUserId } : void 0
155
+ }
156
+ );
157
+ return response.feedback;
158
+ }
159
+ /**
160
+ * Toggle vote on feedback
161
+ */
162
+ async vote(feedbackId, user) {
163
+ return this.request("POST", `/api/v1/feedback/${feedbackId}/vote`, {
164
+ body: user ? { endUser: user } : {}
165
+ });
166
+ }
167
+ /**
168
+ * Check if user has voted on feedback
169
+ */
170
+ async hasVoted(feedbackId, endUserId) {
171
+ return this.request("GET", `/api/v1/feedback/${feedbackId}/vote`, {
172
+ params: endUserId ? { endUserId } : void 0
173
+ });
174
+ }
175
+ };
176
+ var FeedhogApiError = class extends Error {
177
+ constructor(message, status, details) {
178
+ super(message);
179
+ this.status = status;
180
+ this.details = details;
181
+ this.name = "FeedhogApiError";
182
+ }
183
+ };
184
+
185
+ // src/index.ts
186
+ var Feedhog = class extends EventEmitter {
187
+ constructor(config) {
188
+ super();
189
+ this.currentUser = null;
190
+ this.client = new FeedhogClient(config);
191
+ if (isBrowser()) {
192
+ const stored = getStoredUser();
193
+ if (stored) {
194
+ this.currentUser = { ...stored, identified: false };
195
+ }
196
+ }
197
+ }
198
+ /**
199
+ * Get the currently identified user
200
+ */
201
+ get user() {
202
+ return this.currentUser;
203
+ }
204
+ /**
205
+ * Identify the current user
206
+ *
207
+ * This associates feedback and votes with a specific user in your system.
208
+ * User data is persisted in localStorage for subsequent sessions.
209
+ *
210
+ * @param user - User identity data
211
+ * @returns Promise resolving to the identified user
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * await feedhog.identify({
216
+ * externalId: 'user-123',
217
+ * email: 'user@example.com',
218
+ * name: 'John Doe',
219
+ * metadata: { plan: 'pro' }
220
+ * });
221
+ * ```
222
+ */
223
+ async identify(user) {
224
+ try {
225
+ const identified = await this.client.identify(user);
226
+ this.currentUser = {
227
+ ...user,
228
+ identified: true
229
+ };
230
+ if (isBrowser()) {
231
+ storeUser(user);
232
+ }
233
+ this.emit("identify", identified);
234
+ return identified;
235
+ } catch (error) {
236
+ this.emit("error", error);
237
+ throw error;
238
+ }
239
+ }
240
+ /**
241
+ * Submit new feedback
242
+ *
243
+ * If a user has been identified, the feedback will be associated with them.
244
+ *
245
+ * @param input - Feedback data
246
+ * @returns Promise resolving to the created feedback item
247
+ *
248
+ * @example
249
+ * ```typescript
250
+ * const feedback = await feedhog.submit({
251
+ * title: 'Add dark mode',
252
+ * description: 'Would love a dark theme option',
253
+ * type: 'idea'
254
+ * });
255
+ * console.log('Feedback submitted:', feedback.id);
256
+ * ```
257
+ */
258
+ async submit(input) {
259
+ try {
260
+ const feedback = await this.client.submit(
261
+ input,
262
+ this.currentUser || void 0
263
+ );
264
+ this.emit("submit", feedback);
265
+ return feedback;
266
+ } catch (error) {
267
+ this.emit("error", error);
268
+ throw error;
269
+ }
270
+ }
271
+ /**
272
+ * List feedback items
273
+ *
274
+ * Returns a paginated list of feedback for the project.
275
+ *
276
+ * @param options - Filter and pagination options
277
+ * @returns Promise resolving to paginated feedback list
278
+ *
279
+ * @example
280
+ * ```typescript
281
+ * // Get all ideas in "planned" status
282
+ * const { items, total } = await feedhog.list({
283
+ * status: ['planned', 'in-progress'],
284
+ * type: 'idea',
285
+ * sortBy: 'votes',
286
+ * limit: 10
287
+ * });
288
+ * ```
289
+ */
290
+ async list(options) {
291
+ try {
292
+ return await this.client.list(options);
293
+ } catch (error) {
294
+ this.emit("error", error);
295
+ throw error;
296
+ }
297
+ }
298
+ /**
299
+ * Get a single feedback item with full details
300
+ *
301
+ * @param feedbackId - ID of the feedback item
302
+ * @returns Promise resolving to feedback details including comments
303
+ *
304
+ * @example
305
+ * ```typescript
306
+ * const feedback = await feedhog.get('abc123');
307
+ * console.log('Comments:', feedback.comments.length);
308
+ * console.log('Has voted:', feedback.userHasVoted);
309
+ * ```
310
+ */
311
+ async get(feedbackId) {
312
+ try {
313
+ return await this.client.get(
314
+ feedbackId,
315
+ this.currentUser?.externalId
316
+ );
317
+ } catch (error) {
318
+ this.emit("error", error);
319
+ throw error;
320
+ }
321
+ }
322
+ /**
323
+ * Toggle vote on a feedback item
324
+ *
325
+ * If the user has already voted, this will remove their vote.
326
+ * If they haven't voted, it will add their vote.
327
+ *
328
+ * @param feedbackId - ID of the feedback item to vote on
329
+ * @returns Promise resolving to vote result
330
+ *
331
+ * @example
332
+ * ```typescript
333
+ * const { voted, voteCount } = await feedhog.vote('abc123');
334
+ * console.log(voted ? 'Vote added' : 'Vote removed');
335
+ * console.log('Total votes:', voteCount);
336
+ * ```
337
+ */
338
+ async vote(feedbackId) {
339
+ try {
340
+ const result = await this.client.vote(
341
+ feedbackId,
342
+ this.currentUser || void 0
343
+ );
344
+ this.emit("vote", { feedbackId, result });
345
+ return result;
346
+ } catch (error) {
347
+ this.emit("error", error);
348
+ throw error;
349
+ }
350
+ }
351
+ /**
352
+ * Check if the current user has voted on a feedback item
353
+ *
354
+ * @param feedbackId - ID of the feedback item
355
+ * @returns Promise resolving to vote status
356
+ *
357
+ * @example
358
+ * ```typescript
359
+ * const { voted, voteCount } = await feedhog.hasVoted('abc123');
360
+ * ```
361
+ */
362
+ async hasVoted(feedbackId) {
363
+ try {
364
+ return await this.client.hasVoted(
365
+ feedbackId,
366
+ this.currentUser?.externalId
367
+ );
368
+ } catch (error) {
369
+ this.emit("error", error);
370
+ throw error;
371
+ }
372
+ }
373
+ /**
374
+ * Reset the SDK state
375
+ *
376
+ * Clears the current user and removes stored data.
377
+ * Use this when a user logs out.
378
+ *
379
+ * @example
380
+ * ```typescript
381
+ * feedhog.reset();
382
+ * ```
383
+ */
384
+ reset() {
385
+ this.currentUser = null;
386
+ if (isBrowser()) {
387
+ clearStoredUser();
388
+ }
389
+ this.emit("reset", void 0);
390
+ }
391
+ };
392
+ var index_default = Feedhog;
393
+ export {
394
+ Feedhog,
395
+ FeedhogApiError,
396
+ index_default as default
397
+ };
398
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["/**\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","/**\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"],"mappings":"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","names":[]}
@@ -0,0 +1,2 @@
1
+ "use strict";var Feedhog=(()=>{var l=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var U=Object.prototype.hasOwnProperty;var v=(s,e)=>{for(var t in e)l(s,t,{get:e[t],enumerable:!0})},F=(s,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of w(e))!U.call(s,i)&&i!==t&&l(s,i,{get:()=>e[i],enumerable:!(r=k(e,i))||r.enumerable});return s};var I=s=>F(l({},"__esModule",{value:!0}),s);var P={};v(P,{Feedhog:()=>u,FeedhogApiError:()=>a,default:()=>S});var f="feedhog_user";function h(s){if(!(typeof window>"u"||!window.localStorage))try{localStorage.setItem(f,JSON.stringify(s))}catch{}}function g(){if(typeof window>"u"||!window.localStorage)return null;try{let s=localStorage.getItem(f);return s?JSON.parse(s):null}catch{return null}}function m(){if(!(typeof window>"u"||!window.localStorage))try{localStorage.removeItem(f)}catch{}}function b(s,e,t){let r=new URL(e,s);if(t)for(let[i,n]of Object.entries(t))n!==void 0&&(Array.isArray(n)?r.searchParams.set(i,n.join(",")):r.searchParams.set(i,String(n)));return r.toString()}function d(){return typeof window<"u"&&typeof document<"u"}var o=class{constructor(){this.listeners=new Map}on(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>{this.listeners.get(e)?.delete(t)}}emit(e,t){this.listeners.get(e)?.forEach(r=>r(t))}off(e,t){this.listeners.get(e)?.delete(t)}};var E="https://feedhog.com",c=class{constructor(e){if(!e.apiKey)throw new Error("Feedhog: apiKey is required");this.apiKey=e.apiKey,this.baseUrl=(e.baseUrl||E).replace(/\/$/,"")}async request(e,t,r){let i=r?.params?b(this.baseUrl,t,r.params):`${this.baseUrl}${t}`,n=await fetch(i,{method:e,headers:{"Content-Type":"application/json","x-api-key":this.apiKey},body:r?.body?JSON.stringify(r.body):void 0}),y=await n.json();if(!n.ok){let p=y;throw new a(p.error||"Unknown error",n.status,p.details)}return y}async identify(e){return(await this.request("POST","/api/v1/identify",{body:e})).user}async submit(e,t){let r={...e};return t&&(r.endUser=t),(await this.request("POST","/api/v1/feedback",{body:r})).feedback}async list(e){return this.request("GET","/api/v1/feedback",{params:{status:e?.status?Array.isArray(e.status)?e.status:[e.status]:void 0,type:e?.type?Array.isArray(e.type)?e.type:[e.type]:void 0,search:e?.search,sortBy:e?.sortBy,page:e?.page,limit:e?.limit}})}async get(e,t){return(await this.request("GET",`/api/v1/feedback/${e}`,{params:t?{endUserId:t}:void 0})).feedback}async vote(e,t){return this.request("POST",`/api/v1/feedback/${e}/vote`,{body:t?{endUser:t}:{}})}async hasVoted(e,t){return this.request("GET",`/api/v1/feedback/${e}/vote`,{params:t?{endUserId:t}:void 0})}},a=class extends Error{constructor(t,r,i){super(t);this.status=r;this.details=i;this.name="FeedhogApiError"}};var u=class extends o{constructor(t){super();this.currentUser=null;if(this.client=new c(t),d()){let r=g();r&&(this.currentUser={...r,identified:!1})}}get user(){return this.currentUser}async identify(t){try{let r=await this.client.identify(t);return this.currentUser={...t,identified:!0},d()&&h(t),this.emit("identify",r),r}catch(r){throw this.emit("error",r),r}}async submit(t){try{let r=await this.client.submit(t,this.currentUser||void 0);return this.emit("submit",r),r}catch(r){throw this.emit("error",r),r}}async list(t){try{return await this.client.list(t)}catch(r){throw this.emit("error",r),r}}async get(t){try{return await this.client.get(t,this.currentUser?.externalId)}catch(r){throw this.emit("error",r),r}}async vote(t){try{let r=await this.client.vote(t,this.currentUser||void 0);return this.emit("vote",{feedbackId:t,result:r}),r}catch(r){throw this.emit("error",r),r}}async hasVoted(t){try{return await this.client.hasVoted(t,this.currentUser?.externalId)}catch(r){throw this.emit("error",r),r}}reset(){this.currentUser=null,d()&&m(),this.emit("reset",void 0)}},S=u;return I(P);})();
2
+ if(typeof Feedhog!=='undefined'&&Feedhog.default)window.Feedhog=Feedhog.default;
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@feedhog/js",
3
+ "version": "0.1.0",
4
+ "description": "JavaScript SDK for Feedhog - collect user feedback from any website or app",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "dev": "tsup --watch",
27
+ "typecheck": "tsc --noEmit",
28
+ "clean": "rm -rf dist"
29
+ },
30
+ "keywords": [
31
+ "feedhog",
32
+ "feedback",
33
+ "user-feedback",
34
+ "widget",
35
+ "sdk"
36
+ ],
37
+ "author": "Feedhog",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/feedhog/feedhog"
42
+ },
43
+ "homepage": "https://feedhog.com/docs",
44
+ "devDependencies": {
45
+ "tsup": "^8.0.0",
46
+ "typescript": "^5.3.0"
47
+ },
48
+ "peerDependencies": {},
49
+ "engines": {
50
+ "node": ">=18"
51
+ }
52
+ }