create-feedbax 0.2.1 → 0.2.2

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.
Files changed (78) hide show
  1. package/README.md +17 -5
  2. package/dist/cli.js +502 -209
  3. package/dist/index.js +251 -68
  4. package/package.json +7 -7
  5. package/template/.feedbax-template.sha256 +1 -1
  6. package/template/.gitignore.template +1 -0
  7. package/template/.npmignore.template +1 -0
  8. package/template/.nvmrc +1 -0
  9. package/template/AGENTS.md +1 -1
  10. package/template/CONNECTED.md +16 -2
  11. package/template/README.md +52 -13
  12. package/template/feedbax-vite.ts +79 -0
  13. package/template/migrations/0005_github.sql +4 -0
  14. package/template/migrations/0006_integration.sql +3 -0
  15. package/template/migrations/0007_ownership.sql +2 -0
  16. package/template/migrations/0008_integration_recovery.sql +5 -0
  17. package/template/migrations/0009_observed_history.sql +3 -0
  18. package/template/package.json +34 -33
  19. package/template/src/changelog-runtime.ts +2 -1
  20. package/template/src/cloudflare-workers.d.ts +3 -0
  21. package/template/src/comment-creation.test.tsx +53 -6
  22. package/template/src/components/connected-identity-provider.tsx +44 -17
  23. package/template/src/components/follow-post-button.tsx +2 -13
  24. package/template/src/components/post-history.tsx +101 -0
  25. package/template/src/components/profile-menu.tsx +7 -0
  26. package/template/src/configured-feedback.test.ts +82 -0
  27. package/template/src/configured-feedback.ts +61 -0
  28. package/template/src/connected-identity.test.ts +7 -3
  29. package/template/src/connected-identity.ts +6 -1
  30. package/template/src/connected-runtime.test.ts +5 -2
  31. package/template/src/feedback-cache-invalidation.ts +2 -0
  32. package/template/src/feedback-runtime.ts +61 -18
  33. package/template/src/feedbax/changelog/index.test.ts +35 -0
  34. package/template/src/feedbax/changelog/index.ts +5 -1
  35. package/template/src/feedbax/changelog/notion-changelog.test.ts +1 -1
  36. package/template/src/feedbax/changelog/notion-changelog.ts +1 -1
  37. package/template/src/feedbax/config/index.test.ts +45 -1
  38. package/template/src/feedbax/config/index.ts +112 -9
  39. package/template/src/feedbax/feedback/comment-storage.ts +38 -0
  40. package/template/src/feedbax/feedback/connected-feedback.ts +41 -12
  41. package/template/src/feedbax/feedback/connected.ts +15 -5
  42. package/template/src/feedbax/feedback/feedback-storage.ts +1 -0
  43. package/template/src/feedbax/feedback/github-comments.ts +390 -0
  44. package/template/src/feedbax/feedback/github-feedback.test.ts +1138 -0
  45. package/template/src/feedbax/feedback/github-feedback.ts +581 -0
  46. package/template/src/feedbax/feedback/github-request.ts +120 -0
  47. package/template/src/feedbax/feedback/github-service-client.ts +56 -0
  48. package/template/src/feedbax/feedback/image-feedback.ts +23 -10
  49. package/template/src/feedbax/feedback/images.ts +26 -0
  50. package/template/src/feedbax/feedback/index.ts +172 -16
  51. package/template/src/feedbax/feedback/integration-auth.ts +138 -0
  52. package/template/src/feedbax/feedback/notion-comments.ts +2 -35
  53. package/template/src/feedbax/feedback/observed-history.test.ts +150 -0
  54. package/template/src/feedbax/feedback/observed-history.ts +219 -0
  55. package/template/src/feedbax/feedback/ownership.ts +170 -0
  56. package/template/src/history-runtime.test.ts +80 -0
  57. package/template/src/history-runtime.ts +37 -0
  58. package/template/src/index.css +6 -0
  59. package/template/src/integration-runtime.test.ts +365 -0
  60. package/template/src/integration-runtime.ts +289 -0
  61. package/template/src/my-feedback-page.test.tsx +152 -0
  62. package/template/src/my-feedback-page.tsx +398 -0
  63. package/template/src/my-feedback-runtime.test.ts +93 -0
  64. package/template/src/my-feedback-runtime.ts +154 -0
  65. package/template/src/participation-runtime.ts +3 -3
  66. package/template/src/portal-comments.ts +11 -0
  67. package/template/src/portal-feedback-form.tsx +11 -3
  68. package/template/src/portal-feedback-submission.ts +9 -0
  69. package/template/src/portal-server-entry.ts +9 -0
  70. package/template/src/public-portal-error.tsx +1 -1
  71. package/template/src/public-post-detail.tsx +332 -256
  72. package/template/src/public-release-links.test.ts +120 -0
  73. package/template/src/public-release-links.ts +130 -0
  74. package/template/src/routeTree.gen.ts +21 -0
  75. package/template/src/routes/my-feedback.tsx +16 -0
  76. package/template/src/server.ts +46 -1
  77. package/template/src/submission-intent.ts +26 -0
  78. package/template/vite.config.ts +2 -0
@@ -1,3 +1,5 @@
1
+ import { submissionIntent } from "./submission-intent";
2
+ import { PostHistory } from "./components/post-history";
1
3
  import { ImageAttachments, ImageGallery } from "./components/image-attachments";
2
4
  import { FollowPostButton } from "./components/follow-post-button";
3
5
  import {
@@ -10,6 +12,12 @@ import type { PortalFeatures } from "@feedbax/config";
10
12
  import { Skeleton } from "@feedbax/ui/components/skeleton";
11
13
  import { Button } from "@feedbax/ui/components/button";
12
14
  import { Textarea } from "@feedbax/ui/components/textarea";
15
+ import {
16
+ Tabs,
17
+ TabsContent,
18
+ TabsList,
19
+ TabsTrigger,
20
+ } from "@feedbax/ui/components/tabs";
13
21
  import { ArrowLeft } from "lucide-react";
14
22
  import {
15
23
  type FormEvent,
@@ -180,7 +188,8 @@ export function PublicPostDetail({
180
188
  async function createComment(event: FormEvent<HTMLFormElement>) {
181
189
  event.preventDefault();
182
190
  if (!submitComment) return;
183
- const form = new FormData(event.currentTarget);
191
+ const composer = event.currentTarget;
192
+ const form = new FormData(composer);
184
193
  const body = String(form.get("body") ?? "").trim();
185
194
  const profile = completeProfile;
186
195
  if (
@@ -208,11 +217,19 @@ export function PublicPostDetail({
208
217
  };
209
218
  setCommentError(undefined);
210
219
  setLocalComments((current) => addComment(current, optimistic));
211
- event.currentTarget.reset();
212
- setReplyingTo(undefined);
213
220
  try {
214
221
  const turnstileToken = form.get("cf-turnstile-response");
222
+ const intent = await submissionIntent(sessionStorage, {
223
+ kind: "comment",
224
+ slug: post.slug,
225
+ discussionId: replyingTo ?? null,
226
+ body,
227
+ imageIds: images.map((image) => image.id),
228
+ session: identity.sessionKey ?? "browser",
229
+ displayName: identity.enabled ? identity.displayName : profile?.name,
230
+ });
215
231
  const confirmed = await submitComment({
232
+ submissionKey: intent.key,
216
233
  slug: post.slug,
217
234
  ...(images.length ? { imageIds: images.map((image) => image.id) } : {}),
218
235
  body,
@@ -228,10 +245,6 @@ export function PublicPostDetail({
228
245
  }
229
246
  : {}),
230
247
  });
231
- setImages([]);
232
- setLocalComments((current) =>
233
- replaceComment(current, optimisticId, confirmed),
234
- );
235
248
  if (confirmed.commentCapability && confirmed.commentCapabilityExpiresAt)
236
249
  setCommentCapabilities(
237
250
  saveCommentCapability(
@@ -241,6 +254,13 @@ export function PublicPostDetail({
241
254
  confirmed.commentCapabilityExpiresAt,
242
255
  ),
243
256
  );
257
+ intent.complete();
258
+ composer.reset();
259
+ setReplyingTo(undefined);
260
+ setImages([]);
261
+ setLocalComments((current) =>
262
+ replaceComment(current, optimisticId, confirmed),
263
+ );
244
264
  } catch (error) {
245
265
  setLocalComments((current) => removeComment(current, optimisticId));
246
266
  setCommentError(
@@ -329,13 +349,15 @@ export function PublicPostDetail({
329
349
  ) : (
330
350
  <ImageGallery images={post.images} />
331
351
  )}
332
- {post.teamResponse ? (
352
+ {post.teamResponse || post.releaseNotesUrl ? (
333
353
  <section
334
354
  className="border-l-4 border-primary bg-muted/50 p-4"
335
355
  aria-label="Product Team response"
336
356
  >
337
357
  <h2 className="text-sm font-semibold">Product Team response</h2>
338
- <p className="mt-2 whitespace-pre-wrap">{post.teamResponse}</p>
358
+ {post.teamResponse ? (
359
+ <p className="mt-2 whitespace-pre-wrap">{post.teamResponse}</p>
360
+ ) : null}
339
361
  {post.releaseNotesUrl ? (
340
362
  <a
341
363
  className="mt-3 inline-block text-sm font-medium underline underline-offset-4"
@@ -355,275 +377,329 @@ export function PublicPostDetail({
355
377
  ) : null}
356
378
  {published ? <FollowPostButton slug={post.slug} /> : null}
357
379
  </div>
358
- {features.comments ? (
359
- <section
360
- className="post-detail-comments"
361
- aria-labelledby={commentsHeadingId}
362
- >
363
- <h2 id={commentsHeadingId}>Comments</h2>
364
- {submitComment &&
365
- (identity.enabled ? identity.ready : profileReady) &&
366
- canComment ? (
367
- <form
368
- className="comment-composer"
369
- aria-label="Comment composer"
370
- onSubmit={createComment}
380
+ <Tabs
381
+ defaultValue={features.comments ? "comments" : "activities"}
382
+ className="gap-5"
383
+ >
384
+ {published && feedbax.history ? (
385
+ <TabsList
386
+ variant="line"
387
+ activateOnFocus
388
+ aria-label="Post discussion"
389
+ className="h-10 gap-4 border-b p-0"
390
+ >
391
+ {features.comments ? (
392
+ <TabsTrigger value="comments" className="px-1 text-sm">
393
+ Comments
394
+ </TabsTrigger>
395
+ ) : null}
396
+ <TabsTrigger value="activities" className="px-1 text-sm">
397
+ Activities
398
+ </TabsTrigger>
399
+ </TabsList>
400
+ ) : null}
401
+ {features.comments ? (
402
+ <TabsContent value="comments" keepMounted>
403
+ <section
404
+ className={
405
+ published && feedbax.history
406
+ ? "post-detail-comments post-detail-comments-tab"
407
+ : "post-detail-comments"
408
+ }
409
+ aria-labelledby={commentsHeadingId}
371
410
  >
372
- <label
373
- className="comment-composer-label"
374
- htmlFor={`${id}-comment-body`}
411
+ <h2
412
+ id={commentsHeadingId}
413
+ className={
414
+ published && feedbax.history ? "sr-only" : undefined
415
+ }
375
416
  >
376
- {replyingTo ? "Write a reply" : "Add a comment"}
377
- </label>
378
- <p className="comment-composer-hint">
379
- {identity.enabled
380
- ? `Posting as ${identity.displayName}.`
381
- : "Your name is public. Your email stays private."}
382
- </p>
383
- <Textarea
384
- ref={composerInput}
385
- className="comment-composer-input"
386
- id={`${id}-comment-body`}
387
- name="body"
388
- rows={3}
389
- placeholder="Share context, an example, or a question…"
390
- required
391
- />
392
- {turnstileSiteKey &&
393
- !sessionStorage.getItem("feedbax:participation-pass") ? (
394
- <div
395
- ref={(element) => {
396
- turnstileContainer.current = element;
397
- if (
398
- element &&
399
- window.turnstile &&
400
- !turnstileWidget.current
401
- )
402
- turnstileWidget.current = window.turnstile.render(
403
- element,
404
- { sitekey: turnstileSiteKey },
405
- );
406
- }}
407
- className="cf-turnstile"
408
- data-sitekey={turnstileSiteKey}
409
- />
410
- ) : null}
411
- <ImageAttachments
412
- value={images}
413
- onChange={setImages}
414
- onUploadingChange={setUploading}
415
- />
416
- <div className="comment-composer-actions">
417
- <Button
418
- type="submit"
419
- disabled={uploading || submittingComment}
417
+ Comments
418
+ </h2>
419
+ {submitComment &&
420
+ (identity.enabled ? identity.ready : profileReady) &&
421
+ canComment ? (
422
+ <form
423
+ className="comment-composer"
424
+ aria-label="Comment composer"
425
+ onSubmit={createComment}
420
426
  >
421
- {submittingComment
422
- ? "Posting…"
423
- : replyingTo
424
- ? "Post reply"
425
- : "Post comment"}
426
- </Button>
427
- {replyingTo ? (
428
- <Button
429
- type="button"
430
- variant="ghost"
431
- onClick={() => setReplyingTo(undefined)}
427
+ <label
428
+ className="comment-composer-label"
429
+ htmlFor={`${id}-comment-body`}
432
430
  >
433
- Cancel reply
434
- </Button>
435
- ) : null}
436
- </div>
437
- </form>
438
- ) : submitComment && identity.enabled ? (
439
- <ConnectedSignInPrompt />
440
- ) : submitComment ? (
441
- <DeviceProfilePrompt purpose="comment" />
442
- ) : null}
443
- {commentError ? <p role="alert">{commentError}</p> : null}
444
- <div
445
- className="post-detail-thread-list"
446
- role="region"
447
- aria-label="Comment threads"
448
- tabIndex={localComments.length ? 0 : undefined}
449
- >
450
- {commentsLoading && localComments.length === 0 ? (
451
- <PostCommentsSkeleton />
452
- ) : commentsLoadError &&
453
- localComments.length === 0 ? null : localComments.length ===
454
- 0 ? (
455
- <p className="py-5 text-sm text-muted-foreground">
456
- No comments yet
457
- </p>
458
- ) : (
459
- <div className="comment-threads">
460
- {localComments.map((thread) => (
461
- <article className="comment-thread" key={thread.id}>
462
- {thread.comments.map((comment, index) => {
463
- const canChange = Boolean(
464
- mutateComment && commentCapabilities[comment.id],
465
- );
466
- const canReply = Boolean(
467
- index === 0 && submitComment && canComment,
468
- );
469
- return (
470
- <div
471
- className="comment"
472
- data-reply={index > 0 || undefined}
473
- key={comment.id}
474
- >
475
- <header className="comment-header">
476
- <div className="comment-author">
477
- <strong>{comment.author.displayName}</strong>
478
- {comment.author.kind === "participant" ? (
479
- <span>
480
- {comment.author.verified
481
- ? "Verified"
482
- : "Unverified"}
483
- </span>
484
- ) : null}
485
- </div>
486
- <time
487
- dateTime={comment.createdAt.toISOString()}
488
- >
489
- {formatPublicDate(comment.createdAt, "long")}
490
- </time>
491
- </header>
492
- {editingComment?.id === comment.id ? (
493
- <form
494
- className="mt-3 space-y-2"
495
- aria-label="Edit Comment"
496
- onSubmit={(event) => {
497
- event.preventDefault();
498
- void changeComment(
499
- "edit",
500
- { ...comment, discussionId: thread.id },
501
- editingComment.body,
502
- );
503
- }}
504
- >
505
- <Textarea
506
- aria-label="Comment text"
507
- value={editingComment.body}
508
- onChange={(event) =>
509
- setEditingComment({
510
- id: comment.id,
511
- body: event.target.value,
512
- })
513
- }
514
- required
515
- />
516
- <div className="flex gap-2">
517
- <Button size="sm" type="submit">
518
- Save
519
- </Button>
520
- <Button
521
- size="sm"
522
- type="button"
523
- variant="ghost"
524
- onClick={() =>
525
- setEditingComment(undefined)
526
- }
527
- >
528
- Cancel
529
- </Button>
530
- </div>
531
- </form>
532
- ) : (
533
- <>
534
- <p className="comment-body">{comment.body}</p>
535
- <ImageGallery images={comment.images} />
536
- </>
537
- )}
538
- {canChange || canReply ? (
431
+ {replyingTo ? "Write a reply" : "Add a comment"}
432
+ </label>
433
+ <p className="comment-composer-hint">
434
+ {identity.enabled
435
+ ? `Posting as ${identity.displayName}.`
436
+ : "Your name is public. Your email stays private."}
437
+ </p>
438
+ <Textarea
439
+ ref={composerInput}
440
+ className="comment-composer-input"
441
+ id={`${id}-comment-body`}
442
+ name="body"
443
+ rows={3}
444
+ placeholder="Share context, an example, or a question…"
445
+ required
446
+ />
447
+ {turnstileSiteKey &&
448
+ !sessionStorage.getItem("feedbax:participation-pass") ? (
449
+ <div
450
+ ref={(element) => {
451
+ turnstileContainer.current = element;
452
+ if (
453
+ element &&
454
+ window.turnstile &&
455
+ !turnstileWidget.current
456
+ )
457
+ turnstileWidget.current = window.turnstile.render(
458
+ element,
459
+ { sitekey: turnstileSiteKey },
460
+ );
461
+ }}
462
+ className="cf-turnstile"
463
+ data-sitekey={turnstileSiteKey}
464
+ />
465
+ ) : null}
466
+ <ImageAttachments
467
+ value={images}
468
+ onChange={setImages}
469
+ onUploadingChange={setUploading}
470
+ />
471
+ <div className="comment-composer-actions">
472
+ <Button
473
+ type="submit"
474
+ disabled={uploading || submittingComment}
475
+ >
476
+ {submittingComment
477
+ ? "Posting…"
478
+ : replyingTo
479
+ ? "Post reply"
480
+ : "Post comment"}
481
+ </Button>
482
+ {replyingTo ? (
483
+ <Button
484
+ type="button"
485
+ variant="ghost"
486
+ onClick={() => setReplyingTo(undefined)}
487
+ >
488
+ Cancel reply
489
+ </Button>
490
+ ) : null}
491
+ </div>
492
+ </form>
493
+ ) : submitComment && identity.enabled ? (
494
+ <ConnectedSignInPrompt />
495
+ ) : submitComment ? (
496
+ <DeviceProfilePrompt purpose="comment" />
497
+ ) : null}
498
+ {commentError ? <p role="alert">{commentError}</p> : null}
499
+ <div
500
+ className="post-detail-thread-list"
501
+ role="region"
502
+ aria-label="Comment threads"
503
+ tabIndex={localComments.length ? 0 : undefined}
504
+ >
505
+ {commentsLoading && localComments.length === 0 ? (
506
+ <PostCommentsSkeleton />
507
+ ) : commentsLoadError &&
508
+ localComments.length ===
509
+ 0 ? null : localComments.length === 0 ? (
510
+ <p className="py-5 text-sm text-muted-foreground">
511
+ No comments yet
512
+ </p>
513
+ ) : (
514
+ <div className="comment-threads">
515
+ {localComments.map((thread) => (
516
+ <article className="comment-thread" key={thread.id}>
517
+ {thread.comments.map((comment, index) => {
518
+ const canChange = Boolean(
519
+ mutateComment &&
520
+ commentCapabilities[comment.id],
521
+ );
522
+ const canReply = Boolean(
523
+ index === 0 && submitComment && canComment,
524
+ );
525
+ return (
539
526
  <div
540
- className="comment-actions"
541
- role="group"
542
- aria-label={`Comment actions by ${comment.author.displayName}`}
527
+ className="comment"
528
+ data-reply={index > 0 || undefined}
529
+ key={comment.id}
543
530
  >
544
- {canReply ? (
545
- <Button
546
- type="button"
547
- size="sm"
548
- variant="ghost"
549
- onClick={() => {
550
- setReplyingTo(thread.id);
551
- composerInput.current?.focus();
531
+ <header className="comment-header">
532
+ <div className="comment-author">
533
+ <strong>
534
+ {comment.author.displayName}
535
+ </strong>
536
+ {comment.author.kind === "participant" ? (
537
+ <span>
538
+ {comment.author.verified
539
+ ? "Verified"
540
+ : "Unverified"}
541
+ </span>
542
+ ) : null}
543
+ </div>
544
+ <time
545
+ dateTime={comment.createdAt.toISOString()}
546
+ >
547
+ {formatPublicDate(
548
+ comment.createdAt,
549
+ "long",
550
+ )}
551
+ </time>
552
+ </header>
553
+ {editingComment?.id === comment.id ? (
554
+ <form
555
+ className="mt-3 space-y-2"
556
+ aria-label="Edit Comment"
557
+ onSubmit={(event) => {
558
+ event.preventDefault();
559
+ void changeComment(
560
+ "edit",
561
+ {
562
+ ...comment,
563
+ discussionId: thread.id,
564
+ },
565
+ editingComment.body,
566
+ );
552
567
  }}
553
568
  >
554
- Reply
555
- </Button>
556
- ) : null}
557
- {canChange ? (
558
- <>
559
- <Button
560
- type="button"
561
- size="sm"
562
- variant="ghost"
563
- onClick={() =>
569
+ <Textarea
570
+ aria-label="Comment text"
571
+ value={editingComment.body}
572
+ onChange={(event) =>
564
573
  setEditingComment({
565
574
  id: comment.id,
566
- body: comment.body,
567
- })
568
- }
569
- >
570
- Edit
571
- </Button>
572
- <Button
573
- type="button"
574
- size="sm"
575
- variant="ghost"
576
- className="text-destructive hover:text-destructive"
577
- onClick={() =>
578
- void changeComment("delete", {
579
- ...comment,
580
- discussionId: thread.id,
575
+ body: event.target.value,
581
576
  })
582
577
  }
583
- >
584
- Delete
585
- </Button>
578
+ required
579
+ />
580
+ <div className="flex gap-2">
581
+ <Button size="sm" type="submit">
582
+ Save
583
+ </Button>
584
+ <Button
585
+ size="sm"
586
+ type="button"
587
+ variant="ghost"
588
+ onClick={() =>
589
+ setEditingComment(undefined)
590
+ }
591
+ >
592
+ Cancel
593
+ </Button>
594
+ </div>
595
+ </form>
596
+ ) : (
597
+ <>
598
+ <p className="comment-body">
599
+ {comment.body}
600
+ </p>
601
+ <ImageGallery images={comment.images} />
586
602
  </>
603
+ )}
604
+ {canChange || canReply ? (
605
+ <div
606
+ className="comment-actions"
607
+ role="group"
608
+ aria-label={`Comment actions by ${comment.author.displayName}`}
609
+ >
610
+ {canReply ? (
611
+ <Button
612
+ type="button"
613
+ size="sm"
614
+ variant="ghost"
615
+ onClick={() => {
616
+ setReplyingTo(thread.id);
617
+ composerInput.current?.focus();
618
+ }}
619
+ >
620
+ Reply
621
+ </Button>
622
+ ) : null}
623
+ {canChange ? (
624
+ <>
625
+ <Button
626
+ type="button"
627
+ size="sm"
628
+ variant="ghost"
629
+ onClick={() =>
630
+ setEditingComment({
631
+ id: comment.id,
632
+ body: comment.body,
633
+ })
634
+ }
635
+ >
636
+ Edit
637
+ </Button>
638
+ <Button
639
+ type="button"
640
+ size="sm"
641
+ variant="ghost"
642
+ className="text-destructive hover:text-destructive"
643
+ onClick={() =>
644
+ void changeComment("delete", {
645
+ ...comment,
646
+ discussionId: thread.id,
647
+ })
648
+ }
649
+ >
650
+ Delete
651
+ </Button>
652
+ </>
653
+ ) : null}
654
+ </div>
587
655
  ) : null}
588
656
  </div>
589
- ) : null}
590
- </div>
591
- );
592
- })}
593
- </article>
594
- ))}
595
- </div>
596
- )}
597
- {commentsLoadError ? (
598
- <div className="space-y-3 py-4">
599
- <p role="alert" className="text-sm text-muted-foreground">
600
- Comments could not be loaded.
601
- </p>
602
- {retryComments ? (
657
+ );
658
+ })}
659
+ </article>
660
+ ))}
661
+ </div>
662
+ )}
663
+ {commentsLoadError ? (
664
+ <div className="space-y-3 py-4">
665
+ <p
666
+ role="alert"
667
+ className="text-sm text-muted-foreground"
668
+ >
669
+ Comments could not be loaded.
670
+ </p>
671
+ {retryComments ? (
672
+ <Button
673
+ type="button"
674
+ variant="outline"
675
+ size="sm"
676
+ onClick={retryComments}
677
+ >
678
+ Retry Comments
679
+ </Button>
680
+ ) : null}
681
+ </div>
682
+ ) : null}
683
+ {loadMore ? (
603
684
  <Button
604
685
  type="button"
605
686
  variant="outline"
606
- size="sm"
607
- onClick={retryComments}
687
+ disabled={commentsLoading}
688
+ onClick={() => void loadMore()}
608
689
  >
609
- Retry Comments
690
+ {commentsLoading ? "Loading Comments…" : "Load more"}
610
691
  </Button>
611
692
  ) : null}
612
693
  </div>
613
- ) : null}
614
- {loadMore ? (
615
- <Button
616
- type="button"
617
- variant="outline"
618
- disabled={commentsLoading}
619
- onClick={() => void loadMore()}
620
- >
621
- {commentsLoading ? "Loading Comments…" : "Load more"}
622
- </Button>
623
- ) : null}
624
- </div>
625
- </section>
626
- ) : null}
694
+ </section>
695
+ </TabsContent>
696
+ ) : null}
697
+ {published && feedbax.history ? (
698
+ <TabsContent value="activities" keepMounted>
699
+ <PostHistory slug={post.slug} />
700
+ </TabsContent>
701
+ ) : null}
702
+ </Tabs>
627
703
  </div>
628
704
  <aside
629
705
  className="post-detail-sidebar"