@appgram/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,645 @@
1
+ export { useWish } from './chunk-KPIKYXAN.mjs';
2
+ export { HelpArticleDetail, HelpArticles, HelpCenter, HelpCollections, ReleaseCard, ReleaseDetail, ReleaseList, Releases, RoadmapBoard, RoadmapColumn, StatusBoard, StatusIncidentDetail, SubmitWishForm, SupportForm, VoteButton, WhatsNewPopup, WishCard, WishDetail, WishList } from './chunk-AIDLOCVJ.mjs';
3
+ import { getFingerprint, AppgramContext } from './chunk-N6PJDQCU.mjs';
4
+ export { cn, getFingerprint, resetFingerprint, useAppgramContext, useComments, useHelpArticle, useHelpCenter, useHelpFlow, useRelease, useReleases, useRoadmap, useSupport, useVote, useWishes } from './chunk-N6PJDQCU.mjs';
5
+ import { useState, useEffect, useMemo } from 'react';
6
+ import { jsx } from 'react/jsx-runtime';
7
+
8
+ // src/client/AppgramClient.ts
9
+ var AppgramClient = class {
10
+ constructor(config) {
11
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
12
+ this.projectId = config.projectId;
13
+ this.orgSlug = config.orgSlug;
14
+ this.projectSlug = config.projectSlug;
15
+ }
16
+ // ============================================================================
17
+ // HTTP Methods
18
+ // ============================================================================
19
+ async request(method, endpoint, options) {
20
+ let url = `${this.baseUrl}${endpoint}`;
21
+ if (options?.params) {
22
+ const searchParams = new URLSearchParams();
23
+ Object.entries(options.params).forEach(([key, value]) => {
24
+ if (value !== void 0 && value !== null && value !== "") {
25
+ searchParams.append(key, value);
26
+ }
27
+ });
28
+ const queryString = searchParams.toString();
29
+ if (queryString) {
30
+ url += `?${queryString}`;
31
+ }
32
+ }
33
+ try {
34
+ const response = await fetch(url, {
35
+ method,
36
+ headers: {
37
+ "Content-Type": "application/json"
38
+ },
39
+ body: options?.body ? JSON.stringify(options.body) : void 0
40
+ });
41
+ const data = await response.json();
42
+ if (!response.ok) {
43
+ return {
44
+ success: false,
45
+ error: {
46
+ code: String(response.status),
47
+ message: data.message || data.error || "An error occurred"
48
+ }
49
+ };
50
+ }
51
+ if (data && typeof data === "object" && "success" in data) {
52
+ return data;
53
+ }
54
+ return {
55
+ success: true,
56
+ data
57
+ };
58
+ } catch (error) {
59
+ return {
60
+ success: false,
61
+ error: {
62
+ code: "NETWORK_ERROR",
63
+ message: error instanceof Error ? error.message : "Network error"
64
+ }
65
+ };
66
+ }
67
+ }
68
+ get(endpoint, params) {
69
+ return this.request("GET", endpoint, { params });
70
+ }
71
+ /**
72
+ * Raw request that returns the API response as-is without transformation
73
+ */
74
+ async requestRaw(endpoint, params) {
75
+ let url = `${this.baseUrl}${endpoint}`;
76
+ if (params) {
77
+ const searchParams = new URLSearchParams();
78
+ Object.entries(params).forEach(([key, value]) => {
79
+ if (value !== void 0 && value !== null && value !== "") {
80
+ searchParams.append(key, value);
81
+ }
82
+ });
83
+ const queryString = searchParams.toString();
84
+ if (queryString) {
85
+ url += `?${queryString}`;
86
+ }
87
+ }
88
+ try {
89
+ const response = await fetch(url, {
90
+ method: "GET",
91
+ headers: {
92
+ "Content-Type": "application/json"
93
+ }
94
+ });
95
+ const data = await response.json();
96
+ if (!response.ok) {
97
+ return {
98
+ success: false,
99
+ error: {
100
+ code: String(response.status),
101
+ message: data.message || data.error || "An error occurred"
102
+ }
103
+ };
104
+ }
105
+ return data;
106
+ } catch (error) {
107
+ return {
108
+ success: false,
109
+ error: {
110
+ code: "NETWORK_ERROR",
111
+ message: error instanceof Error ? error.message : "Network error"
112
+ }
113
+ };
114
+ }
115
+ }
116
+ post(endpoint, body) {
117
+ return this.request("POST", endpoint, { body });
118
+ }
119
+ delete(endpoint) {
120
+ return this.request("DELETE", endpoint);
121
+ }
122
+ // ============================================================================
123
+ // Wishes
124
+ // ============================================================================
125
+ /**
126
+ * Get public wishes for the project
127
+ */
128
+ async getPublicWishes(filters) {
129
+ const params = {
130
+ project_id: this.projectId
131
+ };
132
+ if (filters?.status) {
133
+ params.status = Array.isArray(filters.status) ? filters.status.join(",") : filters.status;
134
+ }
135
+ if (filters?.category_id) params.category_id = filters.category_id;
136
+ if (filters?.search) params.search = filters.search;
137
+ if (filters?.sort_by) params.sort_by = filters.sort_by;
138
+ if (filters?.sort_order) params.sort_order = filters.sort_order;
139
+ if (filters?.page) params.page = String(filters.page);
140
+ if (filters?.per_page) params.per_page = String(filters.per_page);
141
+ if (filters?.fingerprint) params.fingerprint = filters.fingerprint;
142
+ const rawResponse = await this.requestRaw("/portal/wishes", params);
143
+ if (!rawResponse.success) {
144
+ return {
145
+ success: false,
146
+ error: rawResponse.error
147
+ };
148
+ }
149
+ return {
150
+ success: true,
151
+ data: {
152
+ data: rawResponse.data || [],
153
+ total: rawResponse.total || 0,
154
+ page: rawResponse.page || 1,
155
+ per_page: rawResponse.per_page || 20,
156
+ total_pages: rawResponse.total_pages || 0
157
+ }
158
+ };
159
+ }
160
+ /**
161
+ * Get a single wish by ID
162
+ */
163
+ async getWish(wishId) {
164
+ return this.get(`/portal/wishes/${wishId}`, {
165
+ project_id: this.projectId
166
+ });
167
+ }
168
+ /**
169
+ * Create a new wish (feature request)
170
+ */
171
+ async createWish(data) {
172
+ return this.post("/portal/wishes", {
173
+ project_id: this.projectId,
174
+ ...data
175
+ });
176
+ }
177
+ // ============================================================================
178
+ // Votes
179
+ // ============================================================================
180
+ /**
181
+ * Check if a fingerprint has voted on a wish
182
+ */
183
+ async checkVote(wishId, fingerprint) {
184
+ return this.get(`/api/v1/votes/check/${wishId}`, {
185
+ fingerprint
186
+ });
187
+ }
188
+ /**
189
+ * Create a vote
190
+ */
191
+ async createVote(wishId, fingerprint, voterEmail) {
192
+ return this.post("/api/v1/votes", {
193
+ wish_id: wishId,
194
+ fingerprint,
195
+ voter_email: voterEmail
196
+ });
197
+ }
198
+ /**
199
+ * Delete a vote
200
+ */
201
+ async deleteVote(voteId) {
202
+ return this.delete(`/api/v1/votes/${voteId}`);
203
+ }
204
+ // ============================================================================
205
+ // Comments
206
+ // ============================================================================
207
+ /**
208
+ * Get comments for a wish
209
+ */
210
+ async getComments(wishId, options) {
211
+ const params = {
212
+ wish_id: wishId
213
+ };
214
+ if (options?.page) params.page = String(options.page);
215
+ if (options?.per_page) params.per_page = String(options.per_page);
216
+ const response = await this.get("/api/v1/comments", params);
217
+ if (!response.success) {
218
+ return {
219
+ success: false,
220
+ error: response.error
221
+ };
222
+ }
223
+ const comments = response.data || [];
224
+ return {
225
+ success: true,
226
+ data: {
227
+ data: comments,
228
+ total: comments.length,
229
+ page: options?.page || 1,
230
+ per_page: options?.per_page || 20,
231
+ total_pages: 1
232
+ }
233
+ };
234
+ }
235
+ /**
236
+ * Create a comment
237
+ */
238
+ async createComment(data) {
239
+ return this.post("/api/v1/comments", {
240
+ ...data,
241
+ author_type: "anonymous",
242
+ is_official: false
243
+ });
244
+ }
245
+ // ============================================================================
246
+ // Roadmap
247
+ // ============================================================================
248
+ /**
249
+ * Get roadmap data for the project
250
+ */
251
+ async getRoadmapData() {
252
+ const params = {
253
+ project_id: this.projectId
254
+ };
255
+ if (this.orgSlug && this.projectSlug) {
256
+ params.org_slug = this.orgSlug;
257
+ params.project_slug = this.projectSlug;
258
+ }
259
+ return this.get("/portal/roadmap-data", params);
260
+ }
261
+ // ============================================================================
262
+ // Releases
263
+ // ============================================================================
264
+ /**
265
+ * Get public releases for the project
266
+ */
267
+ async getReleases(options) {
268
+ if (!this.orgSlug || !this.projectSlug) {
269
+ return {
270
+ success: false,
271
+ error: {
272
+ code: "MISSING_SLUGS",
273
+ message: "orgSlug and projectSlug are required for releases endpoint"
274
+ }
275
+ };
276
+ }
277
+ const params = {};
278
+ if (options?.limit) params.limit = String(options.limit);
279
+ return this.get(
280
+ `/api/v1/releases/public/${this.orgSlug}/${this.projectSlug}`,
281
+ params
282
+ );
283
+ }
284
+ /**
285
+ * Get a single release by slug
286
+ */
287
+ async getRelease(releaseSlug) {
288
+ if (!this.orgSlug || !this.projectSlug) {
289
+ return {
290
+ success: false,
291
+ error: {
292
+ code: "MISSING_SLUGS",
293
+ message: "orgSlug and projectSlug are required for releases endpoint"
294
+ }
295
+ };
296
+ }
297
+ return this.get(
298
+ `/api/v1/releases/public/${this.orgSlug}/${this.projectSlug}/${releaseSlug}`
299
+ );
300
+ }
301
+ /**
302
+ * Get features for a release (public endpoint)
303
+ */
304
+ async getReleaseFeatures(releaseSlug) {
305
+ if (!this.orgSlug || !this.projectSlug) {
306
+ return {
307
+ success: false,
308
+ error: {
309
+ code: "MISSING_SLUGS",
310
+ message: "orgSlug and projectSlug are required for release features endpoint"
311
+ }
312
+ };
313
+ }
314
+ return this.get(
315
+ `/api/v1/releases/public/${this.orgSlug}/${this.projectSlug}/${releaseSlug}/features`
316
+ );
317
+ }
318
+ // ============================================================================
319
+ // Help Center
320
+ // ============================================================================
321
+ /**
322
+ * Get help center collection for the project
323
+ */
324
+ async getHelpCollection() {
325
+ return this.get("/portal/help", {
326
+ project_id: this.projectId
327
+ });
328
+ }
329
+ /**
330
+ * Get a help flow by slug
331
+ */
332
+ async getHelpFlow(slug) {
333
+ return this.get(`/portal/help/flows/${slug}`, {
334
+ project_id: this.projectId
335
+ });
336
+ }
337
+ /**
338
+ * Get a help article by slug
339
+ */
340
+ async getHelpArticle(slug, flowId) {
341
+ return this.get(`/portal/help/articles/${slug}`, {
342
+ flow_id: flowId
343
+ });
344
+ }
345
+ // ============================================================================
346
+ // Support
347
+ // ============================================================================
348
+ /**
349
+ * Upload a file via public portal (no auth required)
350
+ */
351
+ async uploadFile(file) {
352
+ const url = `${this.baseUrl}/portal/files/upload`;
353
+ const formData = new FormData();
354
+ formData.append("file", file);
355
+ formData.append("project_id", this.projectId);
356
+ try {
357
+ const response = await fetch(url, {
358
+ method: "POST",
359
+ body: formData
360
+ });
361
+ const data = await response.json();
362
+ if (!response.ok) {
363
+ return {
364
+ success: false,
365
+ error: {
366
+ code: data.error?.code || "UPLOAD_ERROR",
367
+ message: data.error?.message || data.message || "File upload failed"
368
+ }
369
+ };
370
+ }
371
+ return {
372
+ success: true,
373
+ data: data.data || data
374
+ };
375
+ } catch (error) {
376
+ return {
377
+ success: false,
378
+ error: {
379
+ code: "UPLOAD_ERROR",
380
+ message: error instanceof Error ? error.message : "File upload failed"
381
+ }
382
+ };
383
+ }
384
+ }
385
+ /**
386
+ * Submit a support request
387
+ */
388
+ async submitSupportRequest(data) {
389
+ const uploadedAttachments = [];
390
+ if (data.attachments && data.attachments.length > 0) {
391
+ for (const file of data.attachments) {
392
+ if (file.size > 10 * 1024 * 1024) {
393
+ return {
394
+ success: false,
395
+ error: {
396
+ code: "FILE_TOO_LARGE",
397
+ message: `File "${file.name}" is too large. Maximum size is 10MB.`
398
+ }
399
+ };
400
+ }
401
+ const uploadResponse = await this.uploadFile(file);
402
+ if (uploadResponse.success && uploadResponse.data) {
403
+ uploadedAttachments.push({
404
+ url: uploadResponse.data.url,
405
+ name: uploadResponse.data.name,
406
+ size: uploadResponse.data.size,
407
+ mime_type: uploadResponse.data.mime_type
408
+ });
409
+ } else {
410
+ return {
411
+ success: false,
412
+ error: uploadResponse.error || {
413
+ code: "UPLOAD_ERROR",
414
+ message: "Failed to upload attachment"
415
+ }
416
+ };
417
+ }
418
+ }
419
+ }
420
+ const payload = {
421
+ project_id: this.projectId,
422
+ subject: data.subject,
423
+ description: data.description,
424
+ user_email: data.user_email
425
+ };
426
+ if (data.user_name) payload.user_name = data.user_name;
427
+ if (data.category) payload.category = data.category;
428
+ if (uploadedAttachments.length > 0) payload.attachments = uploadedAttachments;
429
+ return this.post("/portal/support-requests", payload);
430
+ }
431
+ /**
432
+ * Request a magic link to access support tickets
433
+ */
434
+ async sendSupportMagicLink(userEmail) {
435
+ return this.post("/portal/support-requests/send-magic-link", {
436
+ project_id: this.projectId,
437
+ user_email: userEmail
438
+ });
439
+ }
440
+ /**
441
+ * Verify magic link token and get user's support tickets
442
+ */
443
+ async verifySupportToken(token) {
444
+ return this.get(
445
+ "/portal/support-requests/verify-token",
446
+ {
447
+ token,
448
+ project_id: this.projectId
449
+ }
450
+ );
451
+ }
452
+ /**
453
+ * Get a specific support ticket using magic link token
454
+ */
455
+ async getSupportTicket(ticketId, token) {
456
+ return this.get(`/portal/support-requests/${ticketId}`, {
457
+ token
458
+ });
459
+ }
460
+ /**
461
+ * Add a message to a support ticket
462
+ */
463
+ async addSupportMessage(ticketId, token, content) {
464
+ const url = `/portal/support-requests/${ticketId}/messages`;
465
+ const fullUrl = `${this.baseUrl}${url}`;
466
+ try {
467
+ const response = await fetch(fullUrl, {
468
+ method: "POST",
469
+ headers: {
470
+ "Content-Type": "application/json",
471
+ "Authorization": `Bearer ${token}`
472
+ },
473
+ body: JSON.stringify({ content })
474
+ });
475
+ const data = await response.json();
476
+ if (!response.ok) {
477
+ return {
478
+ success: false,
479
+ error: {
480
+ code: String(response.status),
481
+ message: data.message || data.error || "An error occurred"
482
+ }
483
+ };
484
+ }
485
+ if (data && typeof data === "object" && "success" in data) {
486
+ return data;
487
+ }
488
+ return {
489
+ success: true,
490
+ data
491
+ };
492
+ } catch (error) {
493
+ return {
494
+ success: false,
495
+ error: {
496
+ code: "NETWORK_ERROR",
497
+ message: error instanceof Error ? error.message : "Network error"
498
+ }
499
+ };
500
+ }
501
+ }
502
+ // ============================================================================
503
+ // Page Data (Combined)
504
+ // ============================================================================
505
+ /**
506
+ * Get all public page data in one request
507
+ */
508
+ async getPageData() {
509
+ const params = {
510
+ project_id: this.projectId
511
+ };
512
+ if (this.orgSlug) params.org_slug = this.orgSlug;
513
+ if (this.projectSlug) params.project_slug = this.projectSlug;
514
+ return this.get("/portal/page-data", params);
515
+ }
516
+ };
517
+ var DEFAULT_API_URL = "https://api.appgram.dev";
518
+ var DEFAULT_LIGHT_COLORS = {
519
+ primary: "#0EA5E9",
520
+ // Arctic blue
521
+ secondary: "#6B7280",
522
+ // Gray
523
+ accent: "#0EA5E9",
524
+ // Arctic blue
525
+ background: "#FFFFFF",
526
+ // White
527
+ text: "#242424",
528
+ // Near-black
529
+ cardBackground: "#F7F7F7",
530
+ cardText: "#242424"
531
+ };
532
+ var DEFAULT_DARK_COLORS = {
533
+ primary: "#38BDF8",
534
+ // Lighter arctic blue
535
+ secondary: "#3A3A3A",
536
+ // Dark gray (subtle for borders)
537
+ accent: "#38BDF8",
538
+ // Lighter arctic blue
539
+ background: "#0A0A0A",
540
+ // Near-black
541
+ text: "#E5E5E5",
542
+ // Light gray
543
+ cardBackground: "#1A1A1A",
544
+ cardText: "#E5E5E5"
545
+ };
546
+ var DEFAULT_THEME = {
547
+ typography: {
548
+ fontFamily: "inherit"
549
+ },
550
+ borderRadius: 8
551
+ };
552
+ function getSystemIsDark() {
553
+ if (typeof window === "undefined") return false;
554
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
555
+ }
556
+ function resolveIsDark(mode) {
557
+ if (mode === "dark") return true;
558
+ if (mode === "light") return false;
559
+ return getSystemIsDark();
560
+ }
561
+ function AppgramProvider({
562
+ config,
563
+ children
564
+ }) {
565
+ const [fingerprint, setFingerprint] = useState(null);
566
+ const themeMode = config.theme?.mode ?? "system";
567
+ const [isDark, setIsDark] = useState(() => resolveIsDark(themeMode));
568
+ useEffect(() => {
569
+ if (config.enableFingerprinting !== false) {
570
+ setFingerprint(getFingerprint());
571
+ }
572
+ }, [config.enableFingerprinting]);
573
+ useEffect(() => {
574
+ if (themeMode !== "system" || typeof window === "undefined") {
575
+ setIsDark(resolveIsDark(themeMode));
576
+ return;
577
+ }
578
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
579
+ setIsDark(mediaQuery.matches);
580
+ const handler = (e) => setIsDark(e.matches);
581
+ mediaQuery.addEventListener("change", handler);
582
+ return () => mediaQuery.removeEventListener("change", handler);
583
+ }, [themeMode]);
584
+ const client = useMemo(() => {
585
+ return new AppgramClient({
586
+ baseUrl: config.apiUrl || DEFAULT_API_URL,
587
+ projectId: config.projectId,
588
+ orgSlug: config.orgSlug,
589
+ projectSlug: config.projectSlug
590
+ });
591
+ }, [config.apiUrl, config.projectId, config.orgSlug, config.projectSlug]);
592
+ const lightColors = useMemo(() => ({
593
+ ...DEFAULT_LIGHT_COLORS,
594
+ ...config.theme?.colors
595
+ }), [config.theme?.colors]);
596
+ const darkColors = useMemo(() => ({
597
+ ...DEFAULT_DARK_COLORS,
598
+ ...config.theme?.darkColors
599
+ }), [config.theme?.darkColors]);
600
+ const currentColors = isDark ? darkColors : lightColors;
601
+ const theme = useMemo(() => {
602
+ return {
603
+ mode: themeMode,
604
+ colors: lightColors,
605
+ darkColors,
606
+ typography: {
607
+ ...DEFAULT_THEME.typography,
608
+ ...config.theme?.typography
609
+ },
610
+ borderRadius: config.theme?.borderRadius ?? DEFAULT_THEME.borderRadius,
611
+ // Include resolved state for components
612
+ isDark,
613
+ currentColors
614
+ };
615
+ }, [themeMode, lightColors, darkColors, config.theme?.typography, config.theme?.borderRadius, isDark, currentColors]);
616
+ const contextValue = useMemo(() => ({
617
+ config: {
618
+ ...config,
619
+ apiUrl: config.apiUrl || DEFAULT_API_URL
620
+ },
621
+ client,
622
+ fingerprint,
623
+ theme
624
+ }), [config, client, fingerprint, theme]);
625
+ useEffect(() => {
626
+ if (typeof document === "undefined") return;
627
+ const root = document.documentElement;
628
+ const colors = currentColors;
629
+ root.setAttribute("data-appgram-theme", isDark ? "dark" : "light");
630
+ if (colors.primary) root.style.setProperty("--appgram-primary", colors.primary);
631
+ if (colors.secondary) root.style.setProperty("--appgram-secondary", colors.secondary);
632
+ if (colors.accent) root.style.setProperty("--appgram-accent", colors.accent);
633
+ if (colors.background) root.style.setProperty("--appgram-background", colors.background);
634
+ if (colors.text) root.style.setProperty("--appgram-foreground", colors.text);
635
+ if (colors.cardBackground) root.style.setProperty("--appgram-card", colors.cardBackground);
636
+ if (colors.cardText) root.style.setProperty("--appgram-card-foreground", colors.cardText);
637
+ if (theme.borderRadius) root.style.setProperty("--appgram-radius", `${theme.borderRadius}px`);
638
+ if (theme.typography?.fontFamily) root.style.setProperty("--appgram-font-family", theme.typography.fontFamily);
639
+ }, [currentColors, isDark, theme.borderRadius, theme.typography?.fontFamily]);
640
+ return /* @__PURE__ */ jsx(AppgramContext.Provider, { value: contextValue, children });
641
+ }
642
+
643
+ export { AppgramClient, AppgramProvider };
644
+ //# sourceMappingURL=index.mjs.map
645
+ //# sourceMappingURL=index.mjs.map