@btst/stack 2.6.2 → 2.8.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 +1 -0
- package/dist/api/index.d.cts +2 -2
- package/dist/api/index.d.mts +2 -2
- package/dist/api/index.d.ts +2 -2
- package/dist/client/index.d.cts +2 -2
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/components/auto-form/index.d.cts +2 -2
- package/dist/components/auto-form/index.d.mts +2 -2
- package/dist/components/auto-form/index.d.ts +2 -2
- package/dist/components/form-builder/index.d.cts +1 -1
- package/dist/components/form-builder/index.d.mts +1 -1
- package/dist/components/form-builder/index.d.ts +1 -1
- package/dist/components/stepped-auto-form/index.d.cts +1 -1
- package/dist/components/stepped-auto-form/index.d.mts +1 -1
- package/dist/components/stepped-auto-form/index.d.ts +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/packages/stack/src/plugins/blog/client/components/loading/post-navigation-skeleton.cjs +13 -0
- package/dist/packages/stack/src/plugins/blog/client/components/loading/post-navigation-skeleton.mjs +11 -0
- package/dist/packages/stack/src/plugins/blog/client/components/loading/recent-posts-carousel-skeleton.cjs +17 -0
- package/dist/packages/stack/src/plugins/blog/client/components/loading/recent-posts-carousel-skeleton.mjs +15 -0
- package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +18 -7
- package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +18 -7
- package/dist/packages/stack/src/plugins/blog/client/components/shared/post-navigation.cjs +48 -52
- package/dist/packages/stack/src/plugins/blog/client/components/shared/post-navigation.mjs +49 -53
- package/dist/packages/stack/src/plugins/blog/client/components/shared/recent-posts-carousel.cjs +34 -37
- package/dist/packages/stack/src/plugins/blog/client/components/shared/recent-posts-carousel.mjs +35 -38
- package/dist/packages/stack/src/plugins/blog/client/hooks/blog-hooks.cjs +4 -21
- package/dist/packages/stack/src/plugins/blog/client/hooks/blog-hooks.mjs +4 -21
- package/dist/packages/stack/src/plugins/comments/api/getters.cjs +284 -0
- package/dist/packages/stack/src/plugins/comments/api/getters.mjs +280 -0
- package/dist/packages/stack/src/plugins/comments/api/mutations.cjs +118 -0
- package/dist/packages/stack/src/plugins/comments/api/mutations.mjs +112 -0
- package/dist/packages/stack/src/plugins/comments/api/plugin.cjs +335 -0
- package/dist/packages/stack/src/plugins/comments/api/plugin.mjs +333 -0
- package/dist/packages/stack/src/plugins/comments/api/query-key-defs.cjs +60 -0
- package/dist/packages/stack/src/plugins/comments/api/query-key-defs.mjs +55 -0
- package/dist/packages/stack/src/plugins/comments/api/serializers.cjs +23 -0
- package/dist/packages/stack/src/plugins/comments/api/serializers.mjs +21 -0
- package/dist/packages/stack/src/plugins/comments/client/components/comment-count.cjs +46 -0
- package/dist/packages/stack/src/plugins/comments/client/components/comment-count.mjs +44 -0
- package/dist/packages/stack/src/plugins/comments/client/components/comment-form.cjs +86 -0
- package/dist/packages/stack/src/plugins/comments/client/components/comment-form.mjs +84 -0
- package/dist/packages/stack/src/plugins/comments/client/components/comment-thread.cjs +540 -0
- package/dist/packages/stack/src/plugins/comments/client/components/comment-thread.mjs +538 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.cjs +64 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.internal.cjs +426 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.internal.mjs +424 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.mjs +62 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.cjs +66 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.internal.cjs +256 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.internal.mjs +254 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.mjs +64 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.cjs +86 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.internal.cjs +191 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.internal.mjs +189 -0
- package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.mjs +84 -0
- package/dist/packages/stack/src/plugins/comments/client/components/shared/page-wrapper.cjs +27 -0
- package/dist/packages/stack/src/plugins/comments/client/components/shared/page-wrapper.mjs +25 -0
- package/dist/packages/stack/src/plugins/comments/client/components/shared/pagination.cjs +37 -0
- package/dist/packages/stack/src/plugins/comments/client/components/shared/pagination.mjs +35 -0
- package/dist/packages/stack/src/plugins/comments/client/hooks/use-comments.cjs +476 -0
- package/dist/packages/stack/src/plugins/comments/client/hooks/use-comments.mjs +464 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/comments-moderation.cjs +67 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/comments-moderation.mjs +65 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/comments-my.cjs +27 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/comments-my.mjs +25 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/comments-thread.cjs +30 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/comments-thread.mjs +28 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/index.cjs +13 -0
- package/dist/packages/stack/src/plugins/comments/client/localization/index.mjs +11 -0
- package/dist/packages/stack/src/plugins/comments/client/plugin.cjs +116 -0
- package/dist/packages/stack/src/plugins/comments/client/plugin.mjs +114 -0
- package/dist/packages/stack/src/plugins/comments/client/utils.cjs +41 -0
- package/dist/packages/stack/src/plugins/comments/client/utils.mjs +37 -0
- package/dist/packages/stack/src/plugins/comments/db.cjs +75 -0
- package/dist/packages/stack/src/plugins/comments/db.mjs +73 -0
- package/dist/packages/stack/src/plugins/comments/schemas.cjs +45 -0
- package/dist/packages/stack/src/plugins/comments/schemas.mjs +38 -0
- package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +5 -4
- package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +5 -4
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.cjs +0 -1
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.mjs +0 -1
- package/dist/packages/stack/src/plugins/kanban/client/components/pages/board-page.internal.cjs +39 -22
- package/dist/packages/stack/src/plugins/kanban/client/components/pages/board-page.internal.mjs +40 -23
- package/dist/packages/ui/src/components/avatar.mjs +1 -1
- package/dist/packages/ui/src/components/pagination-controls.cjs +64 -0
- package/dist/packages/ui/src/components/pagination-controls.mjs +62 -0
- package/dist/packages/ui/src/components/when-visible.cjs +39 -0
- package/dist/packages/ui/src/components/when-visible.mjs +37 -0
- package/dist/plugins/ai-chat/api/index.d.cts +4 -6
- package/dist/plugins/ai-chat/api/index.d.mts +4 -6
- package/dist/plugins/ai-chat/api/index.d.ts +4 -6
- package/dist/plugins/ai-chat/client/hooks/index.d.cts +1 -3
- package/dist/plugins/ai-chat/client/hooks/index.d.mts +1 -3
- package/dist/plugins/ai-chat/client/hooks/index.d.ts +1 -3
- package/dist/plugins/ai-chat/query-keys.d.cts +1 -3
- package/dist/plugins/ai-chat/query-keys.d.mts +1 -3
- package/dist/plugins/ai-chat/query-keys.d.ts +1 -3
- package/dist/plugins/api/index.d.cts +3 -3
- package/dist/plugins/api/index.d.mts +3 -3
- package/dist/plugins/api/index.d.ts +3 -3
- package/dist/plugins/blog/api/index.d.cts +3 -3
- package/dist/plugins/blog/api/index.d.mts +3 -3
- package/dist/plugins/blog/api/index.d.ts +3 -3
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- package/dist/plugins/blog/client/index.d.cts +25 -3
- package/dist/plugins/blog/client/index.d.mts +25 -3
- package/dist/plugins/blog/client/index.d.ts +25 -3
- package/dist/plugins/blog/query-keys.d.cts +3 -3
- package/dist/plugins/blog/query-keys.d.mts +3 -3
- package/dist/plugins/blog/query-keys.d.ts +3 -3
- package/dist/plugins/client/index.d.cts +2 -2
- package/dist/plugins/client/index.d.mts +2 -2
- package/dist/plugins/client/index.d.ts +2 -2
- package/dist/plugins/cms/api/index.d.cts +1 -1
- package/dist/plugins/cms/api/index.d.mts +1 -1
- package/dist/plugins/cms/api/index.d.ts +1 -1
- package/dist/plugins/cms/client/index.d.cts +1 -1
- package/dist/plugins/cms/client/index.d.mts +1 -1
- package/dist/plugins/cms/client/index.d.ts +1 -1
- package/dist/plugins/cms/query-keys.d.cts +1 -1
- package/dist/plugins/cms/query-keys.d.mts +1 -1
- package/dist/plugins/cms/query-keys.d.ts +1 -1
- package/dist/plugins/comments/api/index.cjs +21 -0
- package/dist/plugins/comments/api/index.d.cts +126 -0
- package/dist/plugins/comments/api/index.d.mts +126 -0
- package/dist/plugins/comments/api/index.d.ts +126 -0
- package/dist/plugins/comments/api/index.mjs +5 -0
- package/dist/plugins/comments/client/components/index.cjs +15 -0
- package/dist/plugins/comments/client/components/index.d.cts +125 -0
- package/dist/plugins/comments/client/components/index.d.mts +125 -0
- package/dist/plugins/comments/client/components/index.d.ts +125 -0
- package/dist/plugins/comments/client/components/index.mjs +5 -0
- package/dist/plugins/comments/client/hooks/index.cjs +17 -0
- package/dist/plugins/comments/client/hooks/index.d.cts +200 -0
- package/dist/plugins/comments/client/hooks/index.d.mts +200 -0
- package/dist/plugins/comments/client/hooks/index.d.ts +200 -0
- package/dist/plugins/comments/client/hooks/index.mjs +1 -0
- package/dist/plugins/comments/client/index.cjs +9 -0
- package/dist/plugins/comments/client/index.d.cts +262 -0
- package/dist/plugins/comments/client/index.d.mts +262 -0
- package/dist/plugins/comments/client/index.d.ts +262 -0
- package/dist/plugins/comments/client/index.mjs +2 -0
- package/dist/plugins/comments/client.css +2 -0
- package/dist/plugins/comments/query-keys.cjs +113 -0
- package/dist/plugins/comments/query-keys.d.cts +71 -0
- package/dist/plugins/comments/query-keys.d.mts +71 -0
- package/dist/plugins/comments/query-keys.d.ts +71 -0
- package/dist/plugins/comments/query-keys.mjs +111 -0
- package/dist/plugins/comments/style.css +15 -0
- package/dist/plugins/form-builder/api/index.d.cts +2 -2
- package/dist/plugins/form-builder/api/index.d.mts +2 -2
- package/dist/plugins/form-builder/api/index.d.ts +2 -2
- package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
- package/dist/plugins/form-builder/client/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/index.d.ts +1 -1
- package/dist/plugins/form-builder/query-keys.d.cts +1 -1
- package/dist/plugins/form-builder/query-keys.d.mts +1 -1
- package/dist/plugins/form-builder/query-keys.d.ts +1 -1
- package/dist/plugins/kanban/api/index.d.cts +2 -2
- package/dist/plugins/kanban/api/index.d.mts +2 -2
- package/dist/plugins/kanban/api/index.d.ts +2 -2
- package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
- package/dist/plugins/kanban/client/index.d.cts +1 -1
- package/dist/plugins/kanban/client/index.d.mts +1 -1
- package/dist/plugins/kanban/client/index.d.ts +1 -1
- package/dist/plugins/kanban/query-keys.d.cts +2 -2
- package/dist/plugins/kanban/query-keys.d.mts +2 -2
- package/dist/plugins/kanban/query-keys.d.ts +2 -2
- package/dist/plugins/open-api/api/index.d.cts +3 -3
- package/dist/plugins/open-api/api/index.d.mts +3 -3
- package/dist/plugins/open-api/api/index.d.ts +3 -3
- package/dist/plugins/route-docs/client/index.d.cts +1 -1
- package/dist/plugins/route-docs/client/index.d.mts +1 -1
- package/dist/plugins/route-docs/client/index.d.ts +1 -1
- package/dist/plugins/ui-builder/client/components/index.d.cts +2 -2
- package/dist/plugins/ui-builder/client/components/index.d.mts +2 -2
- package/dist/plugins/ui-builder/client/components/index.d.ts +2 -2
- package/dist/plugins/ui-builder/client/hooks/index.d.cts +3 -3
- package/dist/plugins/ui-builder/client/hooks/index.d.mts +3 -3
- package/dist/plugins/ui-builder/client/hooks/index.d.ts +3 -3
- package/dist/plugins/ui-builder/client/index.d.cts +3 -3
- package/dist/plugins/ui-builder/client/index.d.mts +3 -3
- package/dist/plugins/ui-builder/client/index.d.ts +3 -3
- package/dist/plugins/ui-builder/index.d.cts +3 -3
- package/dist/plugins/ui-builder/index.d.mts +3 -3
- package/dist/plugins/ui-builder/index.d.ts +3 -3
- package/dist/shared/{stack.B1srlBud.d.mts → stack.BFoBvGML.d.mts} +1 -1
- package/dist/shared/{stack.DmpPDPxA.d.cts → stack.BOCvd9HK.d.cts} +1 -1
- package/dist/shared/{stack.n1_i1p2B.d.cts → stack.BOokfhZD.d.cts} +170 -110
- package/dist/shared/{stack.DXnclTG7.d.ts → stack.BSqJrCTM.d.cts} +120 -59
- package/dist/shared/{stack.B58oHdqm.d.mts → stack.BX7MHi0J.d.mts} +90 -45
- package/dist/shared/{stack.cfCkioTe.d.mts → stack.BXxrFL9R.d.ts} +120 -59
- package/dist/shared/{stack.CSx98K5H.d.cts → stack.BYN8wCV6.d.cts} +87 -58
- package/dist/shared/{stack.FVWf2JhZ.d.mts → stack.BgQrdSlo.d.mts} +60 -45
- package/dist/shared/{stack.BK9Z2dcL.d.ts → stack.BmMB0LNC.d.ts} +1 -1
- package/dist/shared/{stack.j75TpKh2.d.ts → stack.BvCR4-9H.d.ts} +170 -110
- package/dist/shared/{stack.FeaWkglm.d.ts → stack.BxFl46lB.d.cts} +24 -1
- package/dist/shared/stack.C-b3Sn8j.d.cts +142 -0
- package/dist/shared/stack.C-b3Sn8j.d.mts +142 -0
- package/dist/shared/stack.C-b3Sn8j.d.ts +142 -0
- package/dist/shared/{stack.CFECM0ew.d.cts → stack.C1nXGBr6.d.cts} +1 -1
- package/dist/shared/{stack.C9Mg2Q46.d.cts → stack.C9zoS1TN.d.cts} +90 -45
- package/dist/shared/stack.CJE9sAjV.d.ts +335 -0
- package/dist/shared/{stack.fdi94T4S.d.mts → stack.CPsYC2-Z.d.cts} +7 -7
- package/dist/shared/{stack.fdi94T4S.d.ts → stack.CPsYC2-Z.d.mts} +7 -7
- package/dist/shared/{stack.fdi94T4S.d.cts → stack.CPsYC2-Z.d.ts} +7 -7
- package/dist/shared/{stack.7n9Y_u7N.d.cts → stack.CQnwAN7x.d.cts} +6 -6
- package/dist/shared/{stack.7n9Y_u7N.d.mts → stack.CQnwAN7x.d.mts} +6 -6
- package/dist/shared/{stack.7n9Y_u7N.d.ts → stack.CQnwAN7x.d.ts} +6 -6
- package/dist/shared/{stack.CxaFNQCV.d.mts → stack.CWxAl9K3.d.mts} +170 -110
- package/dist/shared/{stack.D-b5zbPm.d.cts → stack.Cbsrl06u.d.cts} +60 -45
- package/dist/shared/stack.CmHRdhl8.d.cts +335 -0
- package/dist/shared/{stack.BgTmujxW.d.mts → stack.D88yU4FT.d.mts} +87 -58
- package/dist/shared/{stack.DVtk5CNw.d.mts → stack.DLPa6Gzm.d.mts} +1 -1
- package/dist/shared/{stack.BAT540yW.d.ts → stack.DOZ1EXjM.d.mts} +9 -15
- package/dist/shared/{stack.FeaWkglm.d.mts → stack.DRpeDS6X.d.ts} +24 -1
- package/dist/shared/{stack.B8vT-Yt4.d.mts → stack.DX-tQ93o.d.cts} +9 -15
- package/dist/shared/stack.Dcz6636A.d.mts +335 -0
- package/dist/shared/{stack.ASwEoINr.d.ts → stack.DxJ-tHLt.d.ts} +1 -1
- package/dist/shared/{stack.DaZM10cp.d.cts → stack.DzOhpIYM.d.mts} +120 -59
- package/dist/shared/{stack.CTDVxbrA.d.ts → stack.Fl2Kl_bt.d.ts} +60 -45
- package/dist/shared/{stack.FeaWkglm.d.cts → stack.Jb0kQDJC.d.mts} +24 -1
- package/dist/shared/stack.Ldfkr5b2.d.cts +112 -0
- package/dist/shared/stack.Ldfkr5b2.d.mts +112 -0
- package/dist/shared/stack.Ldfkr5b2.d.ts +112 -0
- package/dist/shared/{stack.CLQuVdwK.d.ts → stack.RuQ9JCLo.d.ts} +87 -58
- package/dist/shared/{stack.BwA7trxA.d.cts → stack.VF6FhyZw.d.ts} +9 -15
- package/dist/shared/{stack.sO33ZDhK.d.ts → stack.fQjVhw5a.d.ts} +90 -45
- package/package.json +70 -5
- package/src/__tests__/plugins.test.tsx +5 -1
- package/src/__tests__/stack-api.test.ts +1 -1
- package/src/plugins/ai-chat/__tests__/getters.test.ts +1 -1
- package/src/plugins/ai-chat/api/getters.ts +1 -1
- package/src/plugins/ai-chat/api/plugin.ts +1 -1
- package/src/plugins/api/index.ts +5 -1
- package/src/plugins/blog/__tests__/getters.test.ts +1 -1
- package/src/plugins/blog/api/getters.ts +1 -1
- package/src/plugins/blog/api/plugin.ts +1 -1
- package/src/plugins/blog/client/components/loading/post-navigation-skeleton.tsx +10 -0
- package/src/plugins/blog/client/components/loading/recent-posts-carousel-skeleton.tsx +18 -0
- package/src/plugins/blog/client/components/pages/post-page.internal.tsx +23 -8
- package/src/plugins/blog/client/components/shared/post-navigation.tsx +0 -5
- package/src/plugins/blog/client/components/shared/recent-posts-carousel.tsx +1 -5
- package/src/plugins/blog/client/hooks/blog-hooks.tsx +8 -33
- package/src/plugins/blog/client/overrides.ts +26 -1
- package/src/plugins/cms/__tests__/getters.test.ts +1 -1
- package/src/plugins/cms/api/getters.ts +1 -1
- package/src/plugins/cms/api/mutations.ts +1 -1
- package/src/plugins/cms/api/plugin.ts +1 -1
- package/src/plugins/cms/client/components/shared/pagination.tsx +14 -42
- package/src/plugins/comments/api/getters.ts +444 -0
- package/src/plugins/comments/api/index.ts +21 -0
- package/src/plugins/comments/api/mutations.ts +206 -0
- package/src/plugins/comments/api/plugin.ts +628 -0
- package/src/plugins/comments/api/query-key-defs.ts +143 -0
- package/src/plugins/comments/api/serializers.ts +37 -0
- package/src/plugins/comments/client/components/comment-count.tsx +66 -0
- package/src/plugins/comments/client/components/comment-form.tsx +112 -0
- package/src/plugins/comments/client/components/comment-thread.tsx +799 -0
- package/src/plugins/comments/client/components/index.tsx +11 -0
- package/src/plugins/comments/client/components/pages/moderation-page.internal.tsx +550 -0
- package/src/plugins/comments/client/components/pages/moderation-page.tsx +70 -0
- package/src/plugins/comments/client/components/pages/my-comments-page.internal.tsx +367 -0
- package/src/plugins/comments/client/components/pages/my-comments-page.tsx +72 -0
- package/src/plugins/comments/client/components/pages/resource-comments-page.internal.tsx +225 -0
- package/src/plugins/comments/client/components/pages/resource-comments-page.tsx +97 -0
- package/src/plugins/comments/client/components/shared/page-wrapper.tsx +32 -0
- package/src/plugins/comments/client/components/shared/pagination.tsx +44 -0
- package/src/plugins/comments/client/hooks/index.tsx +13 -0
- package/src/plugins/comments/client/hooks/use-comments.tsx +717 -0
- package/src/plugins/comments/client/index.ts +14 -0
- package/src/plugins/comments/client/localization/comments-moderation.ts +75 -0
- package/src/plugins/comments/client/localization/comments-my.ts +32 -0
- package/src/plugins/comments/client/localization/comments-thread.ts +32 -0
- package/src/plugins/comments/client/localization/index.ts +11 -0
- package/src/plugins/comments/client/overrides.ts +164 -0
- package/src/plugins/comments/client/plugin.tsx +195 -0
- package/src/plugins/comments/client/utils.ts +67 -0
- package/src/plugins/comments/client.css +2 -0
- package/src/plugins/comments/db.ts +77 -0
- package/src/plugins/comments/query-keys.ts +189 -0
- package/src/plugins/comments/schemas.ts +72 -0
- package/src/plugins/comments/style.css +15 -0
- package/src/plugins/comments/types.ts +73 -0
- package/src/plugins/form-builder/__tests__/getters.test.ts +1 -1
- package/src/plugins/form-builder/api/getters.ts +1 -1
- package/src/plugins/form-builder/api/plugin.ts +1 -1
- package/src/plugins/kanban/__tests__/getters.test.ts +1 -1
- package/src/plugins/kanban/api/getters.ts +1 -1
- package/src/plugins/kanban/api/mutations.ts +1 -1
- package/src/plugins/kanban/api/plugin.ts +6 -5
- package/src/plugins/kanban/client/components/forms/task-form.tsx +0 -1
- package/src/plugins/kanban/client/components/pages/board-page.internal.tsx +46 -27
- package/src/plugins/kanban/client/overrides.ts +27 -1
- package/src/types.ts +5 -1
- package/dist/shared/{stack.BQmuNl5p.d.mts → stack.BWp0hcm9.d.cts} +3 -3
- package/dist/shared/{stack.BQmuNl5p.d.ts → stack.BWp0hcm9.d.mts} +3 -3
- package/dist/shared/{stack.BQmuNl5p.d.cts → stack.BWp0hcm9.d.ts} +3 -3
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, type ComponentType } from "react";
|
|
4
|
+
import { WhenVisible } from "@workspace/ui/components/when-visible";
|
|
5
|
+
import {
|
|
6
|
+
Avatar,
|
|
7
|
+
AvatarFallback,
|
|
8
|
+
AvatarImage,
|
|
9
|
+
} from "@workspace/ui/components/avatar";
|
|
10
|
+
import { Badge } from "@workspace/ui/components/badge";
|
|
11
|
+
import { Button } from "@workspace/ui/components/button";
|
|
12
|
+
import { Separator } from "@workspace/ui/components/separator";
|
|
13
|
+
import {
|
|
14
|
+
Heart,
|
|
15
|
+
MessageSquare,
|
|
16
|
+
Pencil,
|
|
17
|
+
X,
|
|
18
|
+
LogIn,
|
|
19
|
+
ChevronDown,
|
|
20
|
+
ChevronUp,
|
|
21
|
+
} from "lucide-react";
|
|
22
|
+
import { formatDistanceToNow } from "date-fns";
|
|
23
|
+
import type { SerializedComment } from "../../types";
|
|
24
|
+
import { getInitials } from "../utils";
|
|
25
|
+
import { CommentForm } from "./comment-form";
|
|
26
|
+
import {
|
|
27
|
+
useComments,
|
|
28
|
+
useInfiniteComments,
|
|
29
|
+
usePostComment,
|
|
30
|
+
useUpdateComment,
|
|
31
|
+
useDeleteComment,
|
|
32
|
+
useToggleLike,
|
|
33
|
+
} from "../hooks/use-comments";
|
|
34
|
+
import {
|
|
35
|
+
COMMENTS_LOCALIZATION,
|
|
36
|
+
type CommentsLocalization,
|
|
37
|
+
} from "../localization";
|
|
38
|
+
import { usePluginOverrides } from "@btst/stack/context";
|
|
39
|
+
import type { CommentsPluginOverrides } from "../overrides";
|
|
40
|
+
|
|
41
|
+
/** Custom input component props */
|
|
42
|
+
export interface CommentInputProps {
|
|
43
|
+
value: string;
|
|
44
|
+
onChange: (value: string) => void;
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
placeholder?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Custom renderer component props */
|
|
50
|
+
export interface CommentRendererProps {
|
|
51
|
+
body: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Override slot for custom input + renderer */
|
|
55
|
+
export interface CommentComponents {
|
|
56
|
+
Input?: ComponentType<CommentInputProps>;
|
|
57
|
+
Renderer?: ComponentType<CommentRendererProps>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface CommentThreadProps {
|
|
61
|
+
/** The resource this thread is attached to (e.g. post slug, task ID) */
|
|
62
|
+
resourceId: string;
|
|
63
|
+
/** Discriminates resources across plugins (e.g. "blog-post", "kanban-task") */
|
|
64
|
+
resourceType: string;
|
|
65
|
+
/** Base URL for API calls */
|
|
66
|
+
apiBaseURL: string;
|
|
67
|
+
/** Path where the API is mounted */
|
|
68
|
+
apiBasePath: string;
|
|
69
|
+
/** Currently authenticated user ID. Omit for read-only / unauthenticated. */
|
|
70
|
+
currentUserId?: string;
|
|
71
|
+
/**
|
|
72
|
+
* URL to redirect unauthenticated users to.
|
|
73
|
+
* When provided and currentUserId is absent, shows a "Please login to comment" prompt.
|
|
74
|
+
*/
|
|
75
|
+
loginHref?: string;
|
|
76
|
+
/** Optional HTTP headers for API calls (e.g. forwarding cookies) */
|
|
77
|
+
headers?: HeadersInit;
|
|
78
|
+
/** Swap in custom Input / Renderer components */
|
|
79
|
+
components?: CommentComponents;
|
|
80
|
+
/** Optional className applied to the root wrapper */
|
|
81
|
+
className?: string;
|
|
82
|
+
/** Localization strings — defaults to English */
|
|
83
|
+
localization?: Partial<CommentsLocalization>;
|
|
84
|
+
/**
|
|
85
|
+
* Number of top-level comments to load per page.
|
|
86
|
+
* Clicking "Load more" fetches the next page. Default: 10.
|
|
87
|
+
*/
|
|
88
|
+
pageSize?: number;
|
|
89
|
+
/**
|
|
90
|
+
* When false, the comment form and reply buttons are hidden.
|
|
91
|
+
* Overrides the global `allowPosting` from `CommentsPluginOverrides`.
|
|
92
|
+
* Defaults to true.
|
|
93
|
+
*/
|
|
94
|
+
allowPosting?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* When false, the edit button is hidden on comment cards.
|
|
97
|
+
* Overrides the global `allowEditing` from `CommentsPluginOverrides`.
|
|
98
|
+
* Defaults to true.
|
|
99
|
+
*/
|
|
100
|
+
allowEditing?: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const DEFAULT_RENDERER: ComponentType<CommentRendererProps> = ({ body }) => (
|
|
104
|
+
<p className="text-sm whitespace-pre-wrap wrap-break-word">{body}</p>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// ─── Comment Card ─────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
function CommentCard({
|
|
110
|
+
comment,
|
|
111
|
+
currentUserId,
|
|
112
|
+
apiBaseURL,
|
|
113
|
+
apiBasePath,
|
|
114
|
+
resourceId,
|
|
115
|
+
resourceType,
|
|
116
|
+
headers,
|
|
117
|
+
components,
|
|
118
|
+
loc,
|
|
119
|
+
infiniteKey,
|
|
120
|
+
onReplyClick,
|
|
121
|
+
allowPosting,
|
|
122
|
+
allowEditing,
|
|
123
|
+
}: {
|
|
124
|
+
comment: SerializedComment;
|
|
125
|
+
currentUserId?: string;
|
|
126
|
+
apiBaseURL: string;
|
|
127
|
+
apiBasePath: string;
|
|
128
|
+
resourceId: string;
|
|
129
|
+
resourceType: string;
|
|
130
|
+
headers?: HeadersInit;
|
|
131
|
+
components?: CommentComponents;
|
|
132
|
+
loc: CommentsLocalization;
|
|
133
|
+
/** Infinite thread query key — pass for top-level comments so like optimistic
|
|
134
|
+
* updates target the correct InfiniteData cache entry. */
|
|
135
|
+
infiniteKey?: readonly unknown[];
|
|
136
|
+
onReplyClick: (parentId: string) => void;
|
|
137
|
+
allowPosting: boolean;
|
|
138
|
+
allowEditing: boolean;
|
|
139
|
+
}) {
|
|
140
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
141
|
+
const Renderer = components?.Renderer ?? DEFAULT_RENDERER;
|
|
142
|
+
|
|
143
|
+
const config = { apiBaseURL, apiBasePath, headers };
|
|
144
|
+
|
|
145
|
+
const updateMutation = useUpdateComment(config);
|
|
146
|
+
const deleteMutation = useDeleteComment(config);
|
|
147
|
+
const toggleLikeMutation = useToggleLike(config, {
|
|
148
|
+
resourceId,
|
|
149
|
+
resourceType,
|
|
150
|
+
parentId: comment.parentId,
|
|
151
|
+
currentUserId,
|
|
152
|
+
infiniteKey,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const isOwn = currentUserId && comment.authorId === currentUserId;
|
|
156
|
+
const isPending = comment.status === "pending";
|
|
157
|
+
const isApproved = comment.status === "approved";
|
|
158
|
+
|
|
159
|
+
const handleEdit = async (body: string) => {
|
|
160
|
+
await updateMutation.mutateAsync({ id: comment.id, body });
|
|
161
|
+
setIsEditing(false);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const handleDelete = async () => {
|
|
165
|
+
if (!window.confirm(loc.COMMENTS_DELETE_CONFIRM)) return;
|
|
166
|
+
await deleteMutation.mutateAsync(comment.id);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleLike = () => {
|
|
170
|
+
if (!currentUserId) return;
|
|
171
|
+
toggleLikeMutation.mutate({
|
|
172
|
+
commentId: comment.id,
|
|
173
|
+
authorId: currentUserId,
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div
|
|
179
|
+
className="flex gap-3 py-3"
|
|
180
|
+
data-testid="comment-card"
|
|
181
|
+
data-comment-id={comment.id}
|
|
182
|
+
>
|
|
183
|
+
<Avatar className="h-8 w-8 shrink-0 mt-0.5">
|
|
184
|
+
{comment.resolvedAvatarUrl && (
|
|
185
|
+
<AvatarImage
|
|
186
|
+
src={comment.resolvedAvatarUrl}
|
|
187
|
+
alt={comment.resolvedAuthorName}
|
|
188
|
+
/>
|
|
189
|
+
)}
|
|
190
|
+
<AvatarFallback className="text-xs">
|
|
191
|
+
{getInitials(comment.resolvedAuthorName)}
|
|
192
|
+
</AvatarFallback>
|
|
193
|
+
</Avatar>
|
|
194
|
+
|
|
195
|
+
<div className="flex-1 min-w-0">
|
|
196
|
+
<div className="flex flex-wrap items-center gap-2 mb-1">
|
|
197
|
+
<span className="text-sm font-medium">
|
|
198
|
+
{comment.resolvedAuthorName}
|
|
199
|
+
</span>
|
|
200
|
+
<span className="text-xs text-muted-foreground">
|
|
201
|
+
{formatDistanceToNow(new Date(comment.createdAt), {
|
|
202
|
+
addSuffix: true,
|
|
203
|
+
})}
|
|
204
|
+
</span>
|
|
205
|
+
{comment.editedAt && (
|
|
206
|
+
<span className="text-xs text-muted-foreground italic">
|
|
207
|
+
{loc.COMMENTS_EDITED_BADGE}
|
|
208
|
+
</span>
|
|
209
|
+
)}
|
|
210
|
+
{isPending && isOwn && (
|
|
211
|
+
<Badge
|
|
212
|
+
variant="secondary"
|
|
213
|
+
className="text-xs"
|
|
214
|
+
data-testid="pending-badge"
|
|
215
|
+
>
|
|
216
|
+
{loc.COMMENTS_PENDING_BADGE}
|
|
217
|
+
</Badge>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
{isEditing ? (
|
|
222
|
+
<CommentForm
|
|
223
|
+
authorId={currentUserId ?? ""}
|
|
224
|
+
initialBody={comment.body}
|
|
225
|
+
submitLabel={loc.COMMENTS_SAVE_EDIT}
|
|
226
|
+
InputComponent={components?.Input}
|
|
227
|
+
localization={loc}
|
|
228
|
+
onSubmit={handleEdit}
|
|
229
|
+
onCancel={() => setIsEditing(false)}
|
|
230
|
+
/>
|
|
231
|
+
) : (
|
|
232
|
+
<Renderer body={comment.body} />
|
|
233
|
+
)}
|
|
234
|
+
|
|
235
|
+
{!isEditing && (
|
|
236
|
+
<div className="flex items-center gap-1 mt-2">
|
|
237
|
+
{currentUserId && isApproved && (
|
|
238
|
+
<Button
|
|
239
|
+
variant="ghost"
|
|
240
|
+
size="sm"
|
|
241
|
+
className="h-7 px-2 text-xs gap-1"
|
|
242
|
+
onClick={handleLike}
|
|
243
|
+
aria-label={
|
|
244
|
+
comment.isLikedByCurrentUser
|
|
245
|
+
? loc.COMMENTS_UNLIKE_ARIA
|
|
246
|
+
: loc.COMMENTS_LIKE_ARIA
|
|
247
|
+
}
|
|
248
|
+
data-testid="like-button"
|
|
249
|
+
>
|
|
250
|
+
<Heart
|
|
251
|
+
className={`h-3.5 w-3.5 ${comment.isLikedByCurrentUser ? "fill-current text-red-500" : ""}`}
|
|
252
|
+
/>
|
|
253
|
+
{comment.likes > 0 && (
|
|
254
|
+
<span data-testid="like-count">{comment.likes}</span>
|
|
255
|
+
)}
|
|
256
|
+
</Button>
|
|
257
|
+
)}
|
|
258
|
+
|
|
259
|
+
{allowPosting &&
|
|
260
|
+
currentUserId &&
|
|
261
|
+
!comment.parentId &&
|
|
262
|
+
isApproved && (
|
|
263
|
+
<Button
|
|
264
|
+
variant="ghost"
|
|
265
|
+
size="sm"
|
|
266
|
+
className="h-7 px-2 text-xs"
|
|
267
|
+
onClick={() => onReplyClick(comment.id)}
|
|
268
|
+
data-testid="reply-button"
|
|
269
|
+
>
|
|
270
|
+
<MessageSquare className="h-3.5 w-3.5 mr-1" />
|
|
271
|
+
{loc.COMMENTS_REPLY_BUTTON}
|
|
272
|
+
</Button>
|
|
273
|
+
)}
|
|
274
|
+
|
|
275
|
+
{isOwn && (
|
|
276
|
+
<>
|
|
277
|
+
{allowEditing && isApproved && (
|
|
278
|
+
<Button
|
|
279
|
+
variant="ghost"
|
|
280
|
+
size="sm"
|
|
281
|
+
className="h-7 px-2 text-xs"
|
|
282
|
+
onClick={() => setIsEditing(true)}
|
|
283
|
+
data-testid="edit-button"
|
|
284
|
+
>
|
|
285
|
+
<Pencil className="h-3.5 w-3.5 mr-1" />
|
|
286
|
+
{loc.COMMENTS_EDIT_BUTTON}
|
|
287
|
+
</Button>
|
|
288
|
+
)}
|
|
289
|
+
<Button
|
|
290
|
+
variant="ghost"
|
|
291
|
+
size="sm"
|
|
292
|
+
className="h-7 px-2 text-xs text-destructive hover:text-destructive"
|
|
293
|
+
onClick={handleDelete}
|
|
294
|
+
disabled={deleteMutation.isPending}
|
|
295
|
+
data-testid="delete-button"
|
|
296
|
+
>
|
|
297
|
+
<X className="h-3.5 w-3.5 mr-1" />
|
|
298
|
+
{loc.COMMENTS_DELETE_BUTTON}
|
|
299
|
+
</Button>
|
|
300
|
+
</>
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ─── Thread Inner (handles data) ──────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
const DEFAULT_PAGE_SIZE = 100;
|
|
312
|
+
const REPLIES_PAGE_SIZE = 20;
|
|
313
|
+
const OPTIMISTIC_ID_PREFIX = "optimistic-";
|
|
314
|
+
|
|
315
|
+
function CommentThreadInner({
|
|
316
|
+
resourceId,
|
|
317
|
+
resourceType,
|
|
318
|
+
apiBaseURL,
|
|
319
|
+
apiBasePath,
|
|
320
|
+
currentUserId,
|
|
321
|
+
loginHref,
|
|
322
|
+
headers,
|
|
323
|
+
components,
|
|
324
|
+
localization: localizationProp,
|
|
325
|
+
pageSize: pageSizeProp,
|
|
326
|
+
allowPosting: allowPostingProp,
|
|
327
|
+
allowEditing: allowEditingProp,
|
|
328
|
+
}: CommentThreadProps) {
|
|
329
|
+
const overrides = usePluginOverrides<
|
|
330
|
+
CommentsPluginOverrides,
|
|
331
|
+
Partial<CommentsPluginOverrides>
|
|
332
|
+
>("comments", {});
|
|
333
|
+
const pageSize =
|
|
334
|
+
pageSizeProp ?? overrides.defaultCommentPageSize ?? DEFAULT_PAGE_SIZE;
|
|
335
|
+
const allowPosting = allowPostingProp ?? overrides.allowPosting ?? true;
|
|
336
|
+
const allowEditing = allowEditingProp ?? overrides.allowEditing ?? true;
|
|
337
|
+
const loc = { ...COMMENTS_LOCALIZATION, ...localizationProp };
|
|
338
|
+
const [replyingTo, setReplyingTo] = useState<string | null>(null);
|
|
339
|
+
const [expandedReplies, setExpandedReplies] = useState<Set<string>>(
|
|
340
|
+
new Set(),
|
|
341
|
+
);
|
|
342
|
+
const [replyOffsets, setReplyOffsets] = useState<Record<string, number>>({});
|
|
343
|
+
|
|
344
|
+
const config = { apiBaseURL, apiBasePath, headers };
|
|
345
|
+
|
|
346
|
+
const {
|
|
347
|
+
comments,
|
|
348
|
+
total,
|
|
349
|
+
isLoading,
|
|
350
|
+
loadMore,
|
|
351
|
+
hasMore,
|
|
352
|
+
isLoadingMore,
|
|
353
|
+
queryKey: threadQueryKey,
|
|
354
|
+
} = useInfiniteComments(config, {
|
|
355
|
+
resourceId,
|
|
356
|
+
resourceType,
|
|
357
|
+
status: "approved",
|
|
358
|
+
parentId: null,
|
|
359
|
+
currentUserId,
|
|
360
|
+
pageSize,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const postMutation = usePostComment(config, {
|
|
364
|
+
resourceId,
|
|
365
|
+
resourceType,
|
|
366
|
+
currentUserId,
|
|
367
|
+
infiniteKey: threadQueryKey,
|
|
368
|
+
pageSize,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const handlePost = async (body: string) => {
|
|
372
|
+
if (!currentUserId) return;
|
|
373
|
+
await postMutation.mutateAsync({
|
|
374
|
+
body,
|
|
375
|
+
parentId: null,
|
|
376
|
+
});
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const handleReply = async (body: string, parentId: string) => {
|
|
380
|
+
if (!currentUserId) return;
|
|
381
|
+
await postMutation.mutateAsync({
|
|
382
|
+
body,
|
|
383
|
+
parentId,
|
|
384
|
+
limit: REPLIES_PAGE_SIZE,
|
|
385
|
+
offset: replyOffsets[parentId] ?? 0,
|
|
386
|
+
});
|
|
387
|
+
setReplyingTo(null);
|
|
388
|
+
setExpandedReplies((prev) => new Set(prev).add(parentId));
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<div className="space-y-1" data-testid="comment-thread">
|
|
393
|
+
<div className="flex items-center gap-2 mb-4">
|
|
394
|
+
<MessageSquare className="h-5 w-5 text-muted-foreground" />
|
|
395
|
+
<h3 className="font-semibold text-sm">
|
|
396
|
+
{total === 0 ? loc.COMMENTS_TITLE : `${total} ${loc.COMMENTS_TITLE}`}
|
|
397
|
+
</h3>
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
{isLoading && (
|
|
401
|
+
<div className="space-y-4">
|
|
402
|
+
{[1, 2].map((i) => (
|
|
403
|
+
<div key={i} className="flex gap-3 py-3 animate-pulse">
|
|
404
|
+
<div className="h-8 w-8 rounded-full bg-muted shrink-0" />
|
|
405
|
+
<div className="flex-1 space-y-2">
|
|
406
|
+
<div className="h-3 w-24 rounded bg-muted" />
|
|
407
|
+
<div className="h-3 w-full rounded bg-muted" />
|
|
408
|
+
<div className="h-3 w-3/4 rounded bg-muted" />
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
))}
|
|
412
|
+
</div>
|
|
413
|
+
)}
|
|
414
|
+
|
|
415
|
+
{!isLoading && comments.length > 0 && (
|
|
416
|
+
<div className="divide-y divide-border">
|
|
417
|
+
{comments.map((comment) => (
|
|
418
|
+
<div key={comment.id}>
|
|
419
|
+
<CommentCard
|
|
420
|
+
comment={comment}
|
|
421
|
+
currentUserId={currentUserId}
|
|
422
|
+
apiBaseURL={apiBaseURL}
|
|
423
|
+
apiBasePath={apiBasePath}
|
|
424
|
+
resourceId={resourceId}
|
|
425
|
+
resourceType={resourceType}
|
|
426
|
+
headers={headers}
|
|
427
|
+
components={components}
|
|
428
|
+
loc={loc}
|
|
429
|
+
infiniteKey={threadQueryKey}
|
|
430
|
+
onReplyClick={(parentId) => {
|
|
431
|
+
setReplyingTo(replyingTo === parentId ? null : parentId);
|
|
432
|
+
}}
|
|
433
|
+
allowPosting={allowPosting}
|
|
434
|
+
allowEditing={allowEditing}
|
|
435
|
+
/>
|
|
436
|
+
|
|
437
|
+
{/* Replies */}
|
|
438
|
+
<RepliesSection
|
|
439
|
+
parentId={comment.id}
|
|
440
|
+
resourceId={resourceId}
|
|
441
|
+
resourceType={resourceType}
|
|
442
|
+
apiBaseURL={apiBaseURL}
|
|
443
|
+
apiBasePath={apiBasePath}
|
|
444
|
+
currentUserId={currentUserId}
|
|
445
|
+
headers={headers}
|
|
446
|
+
components={components}
|
|
447
|
+
loc={loc}
|
|
448
|
+
expanded={expandedReplies.has(comment.id)}
|
|
449
|
+
replyCount={comment.replyCount}
|
|
450
|
+
onToggle={() => {
|
|
451
|
+
const isExpanded = expandedReplies.has(comment.id);
|
|
452
|
+
if (!isExpanded) {
|
|
453
|
+
setReplyOffsets((prev) => {
|
|
454
|
+
if ((prev[comment.id] ?? 0) === 0) return prev;
|
|
455
|
+
return { ...prev, [comment.id]: 0 };
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
setExpandedReplies((prev) => {
|
|
459
|
+
const next = new Set(prev);
|
|
460
|
+
next.has(comment.id)
|
|
461
|
+
? next.delete(comment.id)
|
|
462
|
+
: next.add(comment.id);
|
|
463
|
+
return next;
|
|
464
|
+
});
|
|
465
|
+
}}
|
|
466
|
+
onOffsetChange={(offset) => {
|
|
467
|
+
setReplyOffsets((prev) => {
|
|
468
|
+
if (prev[comment.id] === offset) return prev;
|
|
469
|
+
return { ...prev, [comment.id]: offset };
|
|
470
|
+
});
|
|
471
|
+
}}
|
|
472
|
+
allowEditing={allowEditing}
|
|
473
|
+
/>
|
|
474
|
+
|
|
475
|
+
{allowPosting && replyingTo === comment.id && currentUserId && (
|
|
476
|
+
<div className="pl-11 pb-3">
|
|
477
|
+
<CommentForm
|
|
478
|
+
authorId={currentUserId}
|
|
479
|
+
parentId={comment.id}
|
|
480
|
+
submitLabel={loc.COMMENTS_FORM_POST_REPLY}
|
|
481
|
+
InputComponent={components?.Input}
|
|
482
|
+
localization={loc}
|
|
483
|
+
onSubmit={(body) => handleReply(body, comment.id)}
|
|
484
|
+
onCancel={() => setReplyingTo(null)}
|
|
485
|
+
/>
|
|
486
|
+
</div>
|
|
487
|
+
)}
|
|
488
|
+
</div>
|
|
489
|
+
))}
|
|
490
|
+
</div>
|
|
491
|
+
)}
|
|
492
|
+
|
|
493
|
+
{!isLoading && comments.length === 0 && (
|
|
494
|
+
<p className="text-sm text-muted-foreground py-4 text-center">
|
|
495
|
+
{loc.COMMENTS_EMPTY}
|
|
496
|
+
</p>
|
|
497
|
+
)}
|
|
498
|
+
|
|
499
|
+
{hasMore && (
|
|
500
|
+
<div className="flex justify-center pt-2">
|
|
501
|
+
<Button
|
|
502
|
+
variant="outline"
|
|
503
|
+
size="sm"
|
|
504
|
+
onClick={() => loadMore()}
|
|
505
|
+
disabled={isLoadingMore}
|
|
506
|
+
data-testid="load-more-comments"
|
|
507
|
+
>
|
|
508
|
+
{isLoadingMore ? loc.COMMENTS_LOADING_MORE : loc.COMMENTS_LOAD_MORE}
|
|
509
|
+
</Button>
|
|
510
|
+
</div>
|
|
511
|
+
)}
|
|
512
|
+
|
|
513
|
+
{allowPosting && (
|
|
514
|
+
<>
|
|
515
|
+
<Separator className="my-4" />
|
|
516
|
+
|
|
517
|
+
{currentUserId ? (
|
|
518
|
+
<div data-testid="comment-form-wrapper">
|
|
519
|
+
<CommentForm
|
|
520
|
+
authorId={currentUserId}
|
|
521
|
+
submitLabel={loc.COMMENTS_FORM_POST_COMMENT}
|
|
522
|
+
InputComponent={components?.Input}
|
|
523
|
+
localization={loc}
|
|
524
|
+
onSubmit={handlePost}
|
|
525
|
+
/>
|
|
526
|
+
</div>
|
|
527
|
+
) : (
|
|
528
|
+
<div
|
|
529
|
+
className="flex flex-col items-center gap-3 py-6 text-center border rounded-lg bg-muted/30"
|
|
530
|
+
data-testid="login-prompt"
|
|
531
|
+
>
|
|
532
|
+
<LogIn className="h-6 w-6 text-muted-foreground" />
|
|
533
|
+
<p className="text-sm text-muted-foreground">
|
|
534
|
+
{loc.COMMENTS_LOGIN_PROMPT}
|
|
535
|
+
</p>
|
|
536
|
+
{loginHref && (
|
|
537
|
+
<a
|
|
538
|
+
href={loginHref}
|
|
539
|
+
className="inline-flex items-center gap-1 text-sm font-medium text-primary underline underline-offset-4"
|
|
540
|
+
data-testid="login-link"
|
|
541
|
+
>
|
|
542
|
+
{loc.COMMENTS_LOGIN_LINK}
|
|
543
|
+
</a>
|
|
544
|
+
)}
|
|
545
|
+
</div>
|
|
546
|
+
)}
|
|
547
|
+
</>
|
|
548
|
+
)}
|
|
549
|
+
</div>
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ─── Replies Section ───────────────────────────────────────────────────────────
|
|
554
|
+
|
|
555
|
+
function RepliesSection({
|
|
556
|
+
parentId,
|
|
557
|
+
resourceId,
|
|
558
|
+
resourceType,
|
|
559
|
+
apiBaseURL,
|
|
560
|
+
apiBasePath,
|
|
561
|
+
currentUserId,
|
|
562
|
+
headers,
|
|
563
|
+
components,
|
|
564
|
+
loc,
|
|
565
|
+
expanded,
|
|
566
|
+
replyCount,
|
|
567
|
+
onToggle,
|
|
568
|
+
onOffsetChange,
|
|
569
|
+
allowEditing,
|
|
570
|
+
}: {
|
|
571
|
+
parentId: string;
|
|
572
|
+
resourceId: string;
|
|
573
|
+
resourceType: string;
|
|
574
|
+
apiBaseURL: string;
|
|
575
|
+
apiBasePath: string;
|
|
576
|
+
currentUserId?: string;
|
|
577
|
+
headers?: HeadersInit;
|
|
578
|
+
components?: CommentComponents;
|
|
579
|
+
loc: CommentsLocalization;
|
|
580
|
+
expanded: boolean;
|
|
581
|
+
/** Pre-computed from the parent comment — avoids an extra fetch on mount. */
|
|
582
|
+
replyCount: number;
|
|
583
|
+
onToggle: () => void;
|
|
584
|
+
onOffsetChange: (offset: number) => void;
|
|
585
|
+
allowEditing: boolean;
|
|
586
|
+
}) {
|
|
587
|
+
const config = { apiBaseURL, apiBasePath, headers };
|
|
588
|
+
const [replyOffset, setReplyOffset] = useState(0);
|
|
589
|
+
const [loadedReplies, setLoadedReplies] = useState<SerializedComment[]>([]);
|
|
590
|
+
// Only fetch reply bodies once the section is expanded.
|
|
591
|
+
const {
|
|
592
|
+
comments: repliesPage,
|
|
593
|
+
total: repliesTotal,
|
|
594
|
+
isFetching: isFetchingReplies,
|
|
595
|
+
} = useComments(
|
|
596
|
+
config,
|
|
597
|
+
{
|
|
598
|
+
resourceId,
|
|
599
|
+
resourceType,
|
|
600
|
+
parentId,
|
|
601
|
+
status: "approved",
|
|
602
|
+
currentUserId,
|
|
603
|
+
limit: REPLIES_PAGE_SIZE,
|
|
604
|
+
offset: replyOffset,
|
|
605
|
+
},
|
|
606
|
+
{ enabled: expanded },
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
useEffect(() => {
|
|
610
|
+
if (expanded) {
|
|
611
|
+
setReplyOffset(0);
|
|
612
|
+
setLoadedReplies([]);
|
|
613
|
+
}
|
|
614
|
+
}, [expanded, parentId]);
|
|
615
|
+
|
|
616
|
+
useEffect(() => {
|
|
617
|
+
onOffsetChange(replyOffset);
|
|
618
|
+
}, [onOffsetChange, replyOffset]);
|
|
619
|
+
|
|
620
|
+
useEffect(() => {
|
|
621
|
+
if (!expanded) return;
|
|
622
|
+
setLoadedReplies((prev) => {
|
|
623
|
+
const byId = new Map(prev.map((item) => [item.id, item]));
|
|
624
|
+
for (const reply of repliesPage) {
|
|
625
|
+
byId.set(reply.id, reply);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Reconcile optimistic replies once the real server reply arrives with
|
|
629
|
+
// a different id. Without this, both entries can persist in local state
|
|
630
|
+
// until the section is collapsed and re-opened.
|
|
631
|
+
const currentPageIds = new Set(repliesPage.map((reply) => reply.id));
|
|
632
|
+
const currentPageRealReplies = repliesPage.filter(
|
|
633
|
+
(reply) => !reply.id.startsWith(OPTIMISTIC_ID_PREFIX),
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
return Array.from(byId.values()).filter((reply) => {
|
|
637
|
+
if (!reply.id.startsWith(OPTIMISTIC_ID_PREFIX)) return true;
|
|
638
|
+
// Keep optimistic items still present in the current cache page.
|
|
639
|
+
if (currentPageIds.has(reply.id)) return true;
|
|
640
|
+
// Drop stale optimistic rows that have been replaced by a real reply.
|
|
641
|
+
return !currentPageRealReplies.some(
|
|
642
|
+
(realReply) =>
|
|
643
|
+
realReply.parentId === reply.parentId &&
|
|
644
|
+
realReply.authorId === reply.authorId &&
|
|
645
|
+
realReply.body === reply.body,
|
|
646
|
+
);
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
}, [expanded, repliesPage]);
|
|
650
|
+
|
|
651
|
+
// Hide when there are no known replies — but keep rendered when already
|
|
652
|
+
// expanded so a freshly-posted first reply (which increments replyCount
|
|
653
|
+
// only after the server responds) stays visible in the same session.
|
|
654
|
+
if (replyCount === 0 && !expanded) return null;
|
|
655
|
+
|
|
656
|
+
// Prefer the fetched count (accurate after optimistic inserts); fall back to
|
|
657
|
+
// the server-provided replyCount before the fetch completes.
|
|
658
|
+
const displayCount = expanded
|
|
659
|
+
? loadedReplies.length || replyCount
|
|
660
|
+
: replyCount;
|
|
661
|
+
const effectiveReplyTotal = repliesTotal || replyCount;
|
|
662
|
+
const hasMoreReplies = loadedReplies.length < effectiveReplyTotal;
|
|
663
|
+
|
|
664
|
+
return (
|
|
665
|
+
<div className="pl-11">
|
|
666
|
+
{/* Toggle button — always at the top so collapse is reachable without scrolling */}
|
|
667
|
+
<Button
|
|
668
|
+
variant="ghost"
|
|
669
|
+
size="sm"
|
|
670
|
+
className="h-7 px-2 text-xs mb-1"
|
|
671
|
+
onClick={onToggle}
|
|
672
|
+
data-testid={expanded ? "hide-replies-button" : "show-replies-button"}
|
|
673
|
+
>
|
|
674
|
+
{expanded ? (
|
|
675
|
+
<ChevronUp className="h-3 w-3 mr-1" />
|
|
676
|
+
) : (
|
|
677
|
+
<ChevronDown className="h-3 w-3 mr-1" />
|
|
678
|
+
)}
|
|
679
|
+
{expanded
|
|
680
|
+
? loc.COMMENTS_HIDE_REPLIES
|
|
681
|
+
: `${displayCount} ${displayCount === 1 ? loc.COMMENTS_REPLIES_SINGULAR : loc.COMMENTS_REPLIES_PLURAL}`}
|
|
682
|
+
</Button>
|
|
683
|
+
{expanded && (
|
|
684
|
+
<div
|
|
685
|
+
className="border-l-2 border-border pl-3 space-y-0"
|
|
686
|
+
data-testid="replies-list"
|
|
687
|
+
>
|
|
688
|
+
{loadedReplies.map((reply) => (
|
|
689
|
+
<CommentCard
|
|
690
|
+
key={reply.id}
|
|
691
|
+
comment={reply}
|
|
692
|
+
currentUserId={currentUserId}
|
|
693
|
+
apiBaseURL={apiBaseURL}
|
|
694
|
+
apiBasePath={apiBasePath}
|
|
695
|
+
resourceId={resourceId}
|
|
696
|
+
resourceType={resourceType}
|
|
697
|
+
headers={headers}
|
|
698
|
+
components={components}
|
|
699
|
+
loc={loc}
|
|
700
|
+
onReplyClick={() => {}} // No nested replies in v1
|
|
701
|
+
allowPosting={false}
|
|
702
|
+
allowEditing={allowEditing}
|
|
703
|
+
/>
|
|
704
|
+
))}
|
|
705
|
+
{hasMoreReplies && (
|
|
706
|
+
<div className="py-2">
|
|
707
|
+
<Button
|
|
708
|
+
variant="ghost"
|
|
709
|
+
size="sm"
|
|
710
|
+
className="h-7 px-2 text-xs"
|
|
711
|
+
onClick={() =>
|
|
712
|
+
setReplyOffset((prev) => prev + REPLIES_PAGE_SIZE)
|
|
713
|
+
}
|
|
714
|
+
disabled={isFetchingReplies}
|
|
715
|
+
data-testid="load-more-replies"
|
|
716
|
+
>
|
|
717
|
+
{isFetchingReplies
|
|
718
|
+
? loc.COMMENTS_LOADING_MORE
|
|
719
|
+
: loc.COMMENTS_LOAD_MORE}
|
|
720
|
+
</Button>
|
|
721
|
+
</div>
|
|
722
|
+
)}
|
|
723
|
+
</div>
|
|
724
|
+
)}
|
|
725
|
+
</div>
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// ─── Public export: lazy-mounts on scroll into view ───────────────────────────
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Embeddable threaded comment section.
|
|
733
|
+
*
|
|
734
|
+
* Lazy-mounts when the component scrolls into the viewport (via WhenVisible).
|
|
735
|
+
* Requires `currentUserId` to allow posting; shows a "Please login" prompt otherwise.
|
|
736
|
+
*
|
|
737
|
+
* @example
|
|
738
|
+
* ```tsx
|
|
739
|
+
* <CommentThread
|
|
740
|
+
* resourceId={post.slug}
|
|
741
|
+
* resourceType="blog-post"
|
|
742
|
+
* apiBaseURL="https://example.com"
|
|
743
|
+
* apiBasePath="/api/data"
|
|
744
|
+
* currentUserId={session?.userId}
|
|
745
|
+
* loginHref="/login"
|
|
746
|
+
* />
|
|
747
|
+
* ```
|
|
748
|
+
*/
|
|
749
|
+
function CommentThreadSkeleton() {
|
|
750
|
+
return (
|
|
751
|
+
<div className="space-y-1">
|
|
752
|
+
{/* Header */}
|
|
753
|
+
<div className="flex items-center gap-2 mb-4">
|
|
754
|
+
<div className="h-5 w-5 rounded bg-muted animate-pulse" />
|
|
755
|
+
<div className="h-4 w-24 rounded bg-muted animate-pulse" />
|
|
756
|
+
</div>
|
|
757
|
+
|
|
758
|
+
{/* Comment rows */}
|
|
759
|
+
{[1, 2, 3].map((i) => (
|
|
760
|
+
<div key={i} className="flex gap-3 py-3">
|
|
761
|
+
<div className="h-8 w-8 rounded-full bg-muted shrink-0 mt-0.5 animate-pulse" />
|
|
762
|
+
<div className="flex-1 space-y-2">
|
|
763
|
+
<div className="flex items-center gap-2">
|
|
764
|
+
<div className="h-3 w-20 rounded bg-muted animate-pulse" />
|
|
765
|
+
<div className="h-3 w-14 rounded bg-muted animate-pulse" />
|
|
766
|
+
</div>
|
|
767
|
+
<div className="h-3 w-full rounded bg-muted animate-pulse" />
|
|
768
|
+
<div className="h-3 w-4/5 rounded bg-muted animate-pulse" />
|
|
769
|
+
<div className="flex gap-1 mt-1">
|
|
770
|
+
<div className="h-7 w-12 rounded bg-muted animate-pulse" />
|
|
771
|
+
<div className="h-7 w-14 rounded bg-muted animate-pulse" />
|
|
772
|
+
</div>
|
|
773
|
+
</div>
|
|
774
|
+
</div>
|
|
775
|
+
))}
|
|
776
|
+
|
|
777
|
+
{/* Separator */}
|
|
778
|
+
<div className="h-px w-full bg-muted my-4" />
|
|
779
|
+
|
|
780
|
+
{/* Textarea placeholder */}
|
|
781
|
+
<div className="space-y-2">
|
|
782
|
+
<div className="h-20 w-full rounded-md border bg-muted animate-pulse" />
|
|
783
|
+
<div className="flex justify-end">
|
|
784
|
+
<div className="h-9 w-28 rounded-md bg-muted animate-pulse" />
|
|
785
|
+
</div>
|
|
786
|
+
</div>
|
|
787
|
+
</div>
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export function CommentThread(props: CommentThreadProps) {
|
|
792
|
+
return (
|
|
793
|
+
<div id="comments" className={props.className}>
|
|
794
|
+
<WhenVisible fallback={<CommentThreadSkeleton />} rootMargin="300px">
|
|
795
|
+
<CommentThreadInner {...props} />
|
|
796
|
+
</WhenVisible>
|
|
797
|
+
</div>
|
|
798
|
+
);
|
|
799
|
+
}
|