@carlonicora/nextjs-jsonapi 5.2.0 → 5.4.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.
Files changed (237) hide show
  1. package/dist/{BlockNoteEditor-EGY4ZAAM.js → BlockNoteEditor-4YCAIEPT.js} +23 -22
  2. package/dist/BlockNoteEditor-4YCAIEPT.js.map +1 -0
  3. package/dist/{BlockNoteEditor-2QJVK2HH.mjs → BlockNoteEditor-YQL2SGFR.mjs} +8 -7
  4. package/dist/BlockNoteEditor-YQL2SGFR.mjs.map +1 -0
  5. package/dist/{BlockNoteViewer-6BQXJ4TB.mjs → BlockNoteViewer-AEJKJ27Q.mjs} +2 -2
  6. package/dist/{BlockNoteViewer-I2MMFH3S.js → BlockNoteViewer-YL3J7J2Y.js} +3 -3
  7. package/dist/{BlockNoteViewer-I2MMFH3S.js.map → BlockNoteViewer-YL3J7J2Y.js.map} +1 -1
  8. package/dist/BlogCategoryFields-DO48vOTE.d.mts +14 -0
  9. package/dist/BlogCategoryFields-DO48vOTE.d.ts +14 -0
  10. package/dist/BlogPostInterface-BB_4wcDR.d.mts +124 -0
  11. package/dist/BlogPostInterface-bWAT1rfR.d.ts +124 -0
  12. package/dist/billing/index.js +441 -441
  13. package/dist/billing/index.mjs +3 -3
  14. package/dist/blog-public.types-BbrzsC1c.d.mts +30 -0
  15. package/dist/blog-public.types-BbrzsC1c.d.ts +30 -0
  16. package/dist/{chunk-3VEQ5JZJ.js → chunk-3NLAXVIN.js} +860 -849
  17. package/dist/{chunk-3VEQ5JZJ.js.map → chunk-3NLAXVIN.js.map} +1 -1
  18. package/dist/{chunk-YMUPTXOT.mjs → chunk-3YW35YHQ.mjs} +1 -101
  19. package/dist/chunk-3YW35YHQ.mjs.map +1 -0
  20. package/dist/{chunk-EXICQC4A.js → chunk-5PPG6PL7.js} +3 -3
  21. package/dist/{chunk-EXICQC4A.js.map → chunk-5PPG6PL7.js.map} +1 -1
  22. package/dist/{chunk-NPKFMRH5.mjs → chunk-6DC2WVVB.mjs} +2 -2
  23. package/dist/{chunk-2Z3ZCYXX.mjs → chunk-EUKTBOI2.mjs} +19 -8
  24. package/dist/chunk-EUKTBOI2.mjs.map +1 -0
  25. package/dist/{chunk-THKYH65N.js → chunk-KSYQEKJX.js} +11 -111
  26. package/dist/chunk-KSYQEKJX.js.map +1 -0
  27. package/dist/{chunk-S5MQDWWC.js → chunk-NFBOSPNJ.js} +1451 -176
  28. package/dist/chunk-NFBOSPNJ.js.map +1 -0
  29. package/dist/{chunk-JEZQTZQ4.mjs → chunk-QMYQRPQC.mjs} +2 -2
  30. package/dist/{chunk-HNOWSFM5.mjs → chunk-WC63PJSC.mjs} +1278 -3
  31. package/dist/chunk-WC63PJSC.mjs.map +1 -0
  32. package/dist/chunk-XCHSX3VC.mjs +107 -0
  33. package/dist/chunk-XCHSX3VC.mjs.map +1 -0
  34. package/dist/{chunk-75MLT74B.js → chunk-YNCQSWQW.js} +8 -8
  35. package/dist/{chunk-75MLT74B.js.map → chunk-YNCQSWQW.js.map} +1 -1
  36. package/dist/chunk-YW4ZCEWB.js +107 -0
  37. package/dist/chunk-YW4ZCEWB.js.map +1 -0
  38. package/dist/client/index.d.mts +6 -39
  39. package/dist/client/index.d.ts +6 -39
  40. package/dist/client/index.js +4 -4
  41. package/dist/client/index.mjs +3 -3
  42. package/dist/components/index.d.mts +15 -4
  43. package/dist/components/index.d.ts +15 -4
  44. package/dist/components/index.js +4 -4
  45. package/dist/components/index.mjs +3 -3
  46. package/dist/contexts/index.js +4 -4
  47. package/dist/contexts/index.mjs +3 -3
  48. package/dist/core/index.d.mts +400 -3
  49. package/dist/core/index.d.ts +400 -3
  50. package/dist/core/index.js +64 -2
  51. package/dist/core/index.js.map +1 -1
  52. package/dist/core/index.mjs +63 -1
  53. package/dist/email-automation-enrollment.fields-BZvF14qx.d.mts +82 -0
  54. package/dist/email-automation-enrollment.fields-Cp3VMzFq.d.ts +82 -0
  55. package/dist/email-automations/index.css +30 -0
  56. package/dist/email-automations/index.css.map +1 -0
  57. package/dist/email-automations/index.d.mts +303 -0
  58. package/dist/email-automations/index.d.ts +303 -0
  59. package/dist/email-automations/index.js +1705 -0
  60. package/dist/email-automations/index.js.map +1 -0
  61. package/dist/email-automations/index.mjs +1705 -0
  62. package/dist/email-automations/index.mjs.map +1 -0
  63. package/dist/features/analytics/index.js +122 -122
  64. package/dist/features/analytics/index.mjs +4 -4
  65. package/dist/features/analytics/server.js +6 -5
  66. package/dist/features/analytics/server.js.map +1 -1
  67. package/dist/features/analytics/server.mjs +4 -3
  68. package/dist/features/analytics/server.mjs.map +1 -1
  69. package/dist/features/blog/index.css +30 -0
  70. package/dist/features/blog/index.css.map +1 -0
  71. package/dist/features/blog/index.d.mts +266 -0
  72. package/dist/features/blog/index.d.ts +266 -0
  73. package/dist/features/blog/index.js +1799 -0
  74. package/dist/features/blog/index.js.map +1 -0
  75. package/dist/features/blog/index.mjs +1799 -0
  76. package/dist/features/blog/index.mjs.map +1 -0
  77. package/dist/features/blog/server.d.mts +33 -0
  78. package/dist/features/blog/server.d.ts +33 -0
  79. package/dist/features/blog/server.js +139 -0
  80. package/dist/features/blog/server.js.map +1 -0
  81. package/dist/features/blog/server.mjs +139 -0
  82. package/dist/features/blog/server.mjs.map +1 -0
  83. package/dist/features/help/index.js +37 -37
  84. package/dist/features/help/index.mjs +3 -3
  85. package/dist/features/tokenusage/index.js +125 -125
  86. package/dist/features/tokenusage/index.mjs +4 -4
  87. package/dist/index.d.mts +5 -2
  88. package/dist/index.d.ts +5 -2
  89. package/dist/index.js +65 -3
  90. package/dist/index.js.map +1 -1
  91. package/dist/index.mjs +64 -2
  92. package/dist/{s3.service-CY-jgOAM.d.ts → s3.service-Bz_K-tjz.d.ts} +11 -0
  93. package/dist/{s3.service-Sri0QP7Z.d.mts → s3.service-D5zNCEW3.d.mts} +11 -0
  94. package/dist/server/index.d.mts +1 -1
  95. package/dist/server/index.d.ts +1 -1
  96. package/dist/server/index.js +8 -6
  97. package/dist/server/index.js.map +1 -1
  98. package/dist/server/index.mjs +12 -10
  99. package/dist/types-70acd-8C.d.mts +39 -0
  100. package/dist/types-l8SFleXi.d.ts +39 -0
  101. package/messages/en.json +210 -0
  102. package/messages/it.json +210 -0
  103. package/package.json +21 -1
  104. package/src/components/editors/BlockNoteEditor.tsx +13 -3
  105. package/src/components/forms/FormBlockNote.tsx +4 -0
  106. package/src/components/forms/FormFieldWrapper.tsx +1 -1
  107. package/src/components/forms/FormTextarea.tsx +2 -1
  108. package/src/components/navigations/ContentTitle.tsx +14 -3
  109. package/src/core/index.ts +12 -0
  110. package/src/core/registry/ModuleRegistry.ts +11 -0
  111. package/src/email-automations/index.ts +5 -0
  112. package/src/features/blog/BlogModules.ts +49 -0
  113. package/src/features/blog/components/admin/BlogAdminContainer.tsx +32 -0
  114. package/src/features/blog/components/admin/BlogCategoriesList.tsx +56 -0
  115. package/src/features/blog/components/admin/BlogCategoryEditor.tsx +230 -0
  116. package/src/features/blog/components/admin/BlogCoverImageField.tsx +123 -0
  117. package/src/features/blog/components/admin/BlogLayoutSetting.tsx +86 -0
  118. package/src/features/blog/components/admin/BlogPostComparison.tsx +54 -0
  119. package/src/features/blog/components/admin/BlogPostContainer.tsx +102 -0
  120. package/src/features/blog/components/admin/BlogPostDeleter.tsx +35 -0
  121. package/src/features/blog/components/admin/BlogPostEditor.tsx +207 -0
  122. package/src/features/blog/components/admin/BlogPostPublishButton.tsx +50 -0
  123. package/src/features/blog/components/admin/BlogPostTranslationEditor.tsx +110 -0
  124. package/src/features/blog/components/admin/BlogPostTranslationPanel.tsx +136 -0
  125. package/src/features/blog/components/admin/BlogPostsList.tsx +58 -0
  126. package/src/features/blog/components/admin/BlogTranslationStatus.tsx +104 -0
  127. package/src/features/blog/components/admin/__tests__/BlogPostTranslationPanel.spec.tsx +100 -0
  128. package/src/features/blog/components/admin/__tests__/BlogPostsList.spec.tsx +30 -0
  129. package/src/features/blog/components/admin/index.ts +14 -0
  130. package/src/features/blog/components/public/BlogCategoryChips.tsx +59 -0
  131. package/src/features/blog/components/public/BlogList.tsx +21 -0
  132. package/src/features/blog/components/public/BlogListColumn.tsx +53 -0
  133. package/src/features/blog/components/public/BlogListFeatured.tsx +57 -0
  134. package/src/features/blog/components/public/BlogListParts.tsx +74 -0
  135. package/src/features/blog/components/public/BlogListRail.tsx +62 -0
  136. package/src/features/blog/components/public/BlogPostCard.tsx +132 -0
  137. package/src/features/blog/components/public/BlogPostMeta.tsx +49 -0
  138. package/src/features/blog/components/public/BlogPostView.tsx +74 -0
  139. package/src/features/blog/components/public/BlogSearchBox.tsx +52 -0
  140. package/src/features/blog/components/public/__tests__/BlogList.spec.tsx +173 -0
  141. package/src/features/blog/components/public/__tests__/BlogPostView.spec.tsx +128 -0
  142. package/src/features/blog/components/public/blogPublicHelpers.ts +30 -0
  143. package/src/features/blog/components/public/index.ts +3 -0
  144. package/src/features/blog/contexts/BlogPostContext.tsx +66 -0
  145. package/src/features/blog/contexts/index.ts +1 -0
  146. package/src/features/blog/data/BlogCategory.ts +93 -0
  147. package/src/features/blog/data/BlogCategoryFields.ts +4 -0
  148. package/src/features/blog/data/BlogCategoryInterface.ts +24 -0
  149. package/src/features/blog/data/BlogCategoryService.ts +54 -0
  150. package/src/features/blog/data/BlogCategoryTranslation.ts +90 -0
  151. package/src/features/blog/data/BlogCategoryTranslationInterface.ts +22 -0
  152. package/src/features/blog/data/BlogCategoryTranslationService.ts +30 -0
  153. package/src/features/blog/data/BlogPost.spec.ts +123 -0
  154. package/src/features/blog/data/BlogPost.ts +159 -0
  155. package/src/features/blog/data/BlogPostFields.ts +7 -0
  156. package/src/features/blog/data/BlogPostInterface.ts +39 -0
  157. package/src/features/blog/data/BlogPostService.ts +82 -0
  158. package/src/features/blog/data/BlogPostTranslation.spec.ts +73 -0
  159. package/src/features/blog/data/BlogPostTranslation.ts +110 -0
  160. package/src/features/blog/data/BlogPostTranslationInterface.ts +27 -0
  161. package/src/features/blog/data/BlogPostTranslationService.ts +30 -0
  162. package/src/features/blog/data/BlogSettings.ts +37 -0
  163. package/src/features/blog/data/BlogSettingsInterface.ts +11 -0
  164. package/src/features/blog/data/BlogSettingsService.ts +24 -0
  165. package/src/features/blog/data/index.ts +17 -0
  166. package/src/features/blog/hooks/index.ts +3 -0
  167. package/src/features/blog/hooks/useBlogCategoryNames.ts +34 -0
  168. package/src/features/blog/hooks/useBlogCategoryTableStructure.tsx +84 -0
  169. package/src/features/blog/hooks/useBlogPostTableStructure.tsx +126 -0
  170. package/src/features/blog/index.ts +15 -0
  171. package/src/features/blog/server/fetchers.ts +120 -0
  172. package/src/features/blog/server/toPublic.spec.ts +91 -0
  173. package/src/features/blog/server/toPublic.ts +36 -0
  174. package/src/features/blog/server-entry.ts +17 -0
  175. package/src/features/blog/types/blog-public.types.ts +34 -0
  176. package/src/features/blog/types/blog.constants.ts +40 -0
  177. package/src/features/email-automation/EmailAutomationEnrollmentModule.ts +11 -0
  178. package/src/features/email-automation/EmailAutomationModule.ts +30 -0
  179. package/src/features/email-automation/EmailAutomationStepModule.ts +11 -0
  180. package/src/features/email-automation/EmailAutomationTriggerModule.ts +11 -0
  181. package/src/features/email-automation/components/__tests__/EmailAutomationEditor.spec.tsx +104 -0
  182. package/src/features/email-automation/components/canvas/EmailAutomationActiveSwitch.tsx +49 -0
  183. package/src/features/email-automation/components/canvas/EmailAutomationCanvas.tsx +474 -0
  184. package/src/features/email-automation/components/canvas/EmailAutomationCanvasInspector.tsx +89 -0
  185. package/src/features/email-automation/components/canvas/EmailAutomationCanvasTab.tsx +32 -0
  186. package/src/features/email-automation/components/canvas/__tests__/buildCanvasGraph.spec.ts +67 -0
  187. package/src/features/email-automation/components/canvas/buildCanvasGraph.ts +141 -0
  188. package/src/features/email-automation/components/canvas/edges/DelayEdge.tsx +68 -0
  189. package/src/features/email-automation/components/canvas/nodes/EmailStepNode.tsx +43 -0
  190. package/src/features/email-automation/components/canvas/nodes/TriggerNode.tsx +23 -0
  191. package/src/features/email-automation/components/canvas/shared/CanvasToolbar.tsx +53 -0
  192. package/src/features/email-automation/components/canvas/shared/EmptyCanvas.tsx +11 -0
  193. package/src/features/email-automation/components/canvas/shared/__tests__/autoLayout.spec.ts +42 -0
  194. package/src/features/email-automation/components/canvas/shared/__tests__/longPress.spec.ts +51 -0
  195. package/src/features/email-automation/components/canvas/shared/autoLayout.ts +55 -0
  196. package/src/features/email-automation/components/canvas/shared/index.ts +4 -0
  197. package/src/features/email-automation/components/canvas/shared/longPress.ts +57 -0
  198. package/src/features/email-automation/components/containers/EmailAutomationContainer.tsx +80 -0
  199. package/src/features/email-automation/components/containers/EmailAutomationsListContainer.tsx +27 -0
  200. package/src/features/email-automation/components/forms/EmailAutomationEditor.tsx +153 -0
  201. package/src/features/email-automation/components/forms/EmailStepEditor.tsx +151 -0
  202. package/src/features/email-automation/components/index.ts +15 -0
  203. package/src/features/email-automation/components/lists/EmailAutomationRecipientsList.tsx +90 -0
  204. package/src/features/email-automation/components/lists/EmailAutomationsList.tsx +76 -0
  205. package/src/features/email-automation/components/unsubscribe/EmailUnsubscribe.tsx +99 -0
  206. package/src/features/email-automation/components/unsubscribe/__tests__/EmailUnsubscribe.spec.tsx +55 -0
  207. package/src/features/email-automation/contexts/EmailAutomationContext.tsx +175 -0
  208. package/src/features/email-automation/data/EmailAutomation.ts +87 -0
  209. package/src/features/email-automation/data/EmailAutomationEnrollment.ts +105 -0
  210. package/src/features/email-automation/data/EmailAutomationEnrollmentInterface.ts +18 -0
  211. package/src/features/email-automation/data/EmailAutomationEnrollmentService.ts +31 -0
  212. package/src/features/email-automation/data/EmailAutomationInterface.ts +22 -0
  213. package/src/features/email-automation/data/EmailAutomationService.ts +106 -0
  214. package/src/features/email-automation/data/EmailAutomationStep.ts +104 -0
  215. package/src/features/email-automation/data/EmailAutomationStepInterface.ts +23 -0
  216. package/src/features/email-automation/data/EmailAutomationStepService.ts +80 -0
  217. package/src/features/email-automation/data/EmailAutomationTrigger.ts +28 -0
  218. package/src/features/email-automation/data/EmailAutomationTriggerInterface.ts +6 -0
  219. package/src/features/email-automation/data/__tests__/email-automation-models.spec.ts +103 -0
  220. package/src/features/email-automation/data/email-automation-enrollment.fields.ts +8 -0
  221. package/src/features/email-automation/data/email-automation.fields.ts +9 -0
  222. package/src/features/email-automation/data/index.ts +13 -0
  223. package/src/features/email-automation/hooks/index.ts +2 -0
  224. package/src/features/email-automation/hooks/useEmailAutomationEnrollmentTableStructure.tsx +126 -0
  225. package/src/features/email-automation/hooks/useEmailAutomationTableStructure.tsx +119 -0
  226. package/src/features/email-automation/index.ts +5 -0
  227. package/src/features/s3/data/s3.service.ts +17 -0
  228. package/dist/BlockNoteEditor-2QJVK2HH.mjs.map +0 -1
  229. package/dist/BlockNoteEditor-EGY4ZAAM.js.map +0 -1
  230. package/dist/chunk-2Z3ZCYXX.mjs.map +0 -1
  231. package/dist/chunk-HNOWSFM5.mjs.map +0 -1
  232. package/dist/chunk-S5MQDWWC.js.map +0 -1
  233. package/dist/chunk-THKYH65N.js.map +0 -1
  234. package/dist/chunk-YMUPTXOT.mjs.map +0 -1
  235. /package/dist/{BlockNoteViewer-6BQXJ4TB.mjs.map → BlockNoteViewer-AEJKJ27Q.mjs.map} +0 -0
  236. /package/dist/{chunk-NPKFMRH5.mjs.map → chunk-6DC2WVVB.mjs.map} +0 -0
  237. /package/dist/{chunk-JEZQTZQ4.mjs.map → chunk-QMYQRPQC.mjs.map} +0 -0
@@ -0,0 +1,39 @@
1
+ import { ApiDataInterface } from "../../../core";
2
+ import { UserInterface } from "../../user/data/user.interface";
3
+ import { BlogPostStatus } from "../types/blog.constants";
4
+ import { BlogCategoryInterface } from "./BlogCategoryInterface";
5
+ import { BlogPostTranslationInterface } from "./BlogPostTranslationInterface";
6
+
7
+ export type BlogPostInput = {
8
+ id: string;
9
+ originalLanguage?: string;
10
+ title?: string;
11
+ slug?: string;
12
+ summary?: string;
13
+ body?: any;
14
+ coverImage?: string;
15
+ coverImageAlt?: string;
16
+ categoryIds?: string[];
17
+ };
18
+
19
+ export interface BlogPostInterface extends ApiDataInterface {
20
+ get status(): BlogPostStatus;
21
+ get originalLanguage(): string;
22
+ get coverImage(): string | undefined;
23
+ get publishedAt(): Date | undefined;
24
+ get author(): UserInterface | undefined;
25
+ get categories(): BlogCategoryInterface[];
26
+ get translations(): BlogPostTranslationInterface[];
27
+ get original(): BlogPostTranslationInterface | undefined;
28
+
29
+ // Resolved for one language by the public and list queries.
30
+ get language(): string | undefined;
31
+ get title(): string | undefined;
32
+ get slug(): string | undefined;
33
+ get summary(): string | undefined;
34
+ get body(): any;
35
+ get coverImageAlt(): string | undefined;
36
+ get readingMinutes(): number | undefined;
37
+ get authorName(): string | undefined;
38
+ get categoryIds(): string[];
39
+ }
@@ -0,0 +1,82 @@
1
+ import { AbstractService, EndpointCreator, HttpMethod, Modules, NextRef } from "../../../core";
2
+ import { BLOG_ACTION } from "../types/blog.constants";
3
+ import { BlogPostInput, BlogPostInterface } from "./BlogPostInterface";
4
+
5
+ export class BlogPostService extends AbstractService {
6
+ static async findMany(params: { search?: string; next?: NextRef } = {}): Promise<BlogPostInterface[]> {
7
+ const endpoint = new EndpointCreator({ endpoint: Modules.BlogPost });
8
+
9
+ if (params.search) endpoint.addAdditionalParam("search", params.search);
10
+
11
+ return this.callApi<BlogPostInterface[]>({
12
+ type: Modules.BlogPost,
13
+ method: HttpMethod.GET,
14
+ endpoint: endpoint.generate(),
15
+ next: params.next,
16
+ });
17
+ }
18
+
19
+ static async findOne(params: { id: string }): Promise<BlogPostInterface> {
20
+ return this.callApi<BlogPostInterface>({
21
+ type: Modules.BlogPost,
22
+ method: HttpMethod.GET,
23
+ endpoint: new EndpointCreator({ endpoint: Modules.BlogPost, id: params.id }).generate(),
24
+ });
25
+ }
26
+
27
+ static async create(params: BlogPostInput): Promise<BlogPostInterface> {
28
+ return this.callApi<BlogPostInterface>({
29
+ type: Modules.BlogPost,
30
+ method: HttpMethod.POST,
31
+ endpoint: new EndpointCreator({ endpoint: Modules.BlogPost }).generate(),
32
+ input: params,
33
+ });
34
+ }
35
+
36
+ static async update(params: BlogPostInput): Promise<BlogPostInterface> {
37
+ return this.callApi<BlogPostInterface>({
38
+ type: Modules.BlogPost,
39
+ method: HttpMethod.PUT,
40
+ endpoint: new EndpointCreator({ endpoint: Modules.BlogPost, id: params.id }).generate(),
41
+ input: params,
42
+ });
43
+ }
44
+
45
+ static async delete(params: { id: string }): Promise<void> {
46
+ await this.callApi({
47
+ type: Modules.BlogPost,
48
+ method: HttpMethod.DELETE,
49
+ endpoint: new EndpointCreator({ endpoint: Modules.BlogPost, id: params.id }).generate(),
50
+ });
51
+ }
52
+
53
+ /**
54
+ * POST blog-posts/:id/publish. Sends no body; replies with the post.
55
+ */
56
+ static async publish(params: { id: string }): Promise<BlogPostInterface> {
57
+ return this.callApi<BlogPostInterface>({
58
+ type: Modules.BlogPost,
59
+ method: HttpMethod.POST,
60
+ endpoint: new EndpointCreator({
61
+ endpoint: Modules.BlogPost,
62
+ id: params.id,
63
+ childEndpoint: BLOG_ACTION.publish,
64
+ }).generate(),
65
+ });
66
+ }
67
+
68
+ /**
69
+ * POST blog-posts/:id/unpublish. Sends no body; replies with the post.
70
+ */
71
+ static async unpublish(params: { id: string }): Promise<BlogPostInterface> {
72
+ return this.callApi<BlogPostInterface>({
73
+ type: Modules.BlogPost,
74
+ method: HttpMethod.POST,
75
+ endpoint: new EndpointCreator({
76
+ endpoint: Modules.BlogPost,
77
+ id: params.id,
78
+ childEndpoint: BLOG_ACTION.unpublish,
79
+ }).generate(),
80
+ });
81
+ }
82
+ }
@@ -0,0 +1,73 @@
1
+ import { beforeAll, describe, expect, it } from "vitest";
2
+ import { ModuleRegistry } from "../../../core/registry/ModuleRegistry";
3
+ import { ModuleFactory, ModuleWithPermissions } from "../../../permissions/types";
4
+ import { BlogPostModule, BlogPostTranslationModule } from "../BlogModules";
5
+ import { BlogPostTranslation } from "./BlogPostTranslation";
6
+
7
+ const moduleFactory: ModuleFactory = (params) =>
8
+ ({
9
+ pageUrl: params.pageUrl,
10
+ name: params.name,
11
+ model: params.model,
12
+ }) as ModuleWithPermissions;
13
+
14
+ beforeAll(() => {
15
+ ModuleRegistry.register("BlogPost", BlogPostModule(moduleFactory));
16
+ ModuleRegistry.register("BlogPostTranslation", BlogPostTranslationModule(moduleFactory));
17
+ });
18
+
19
+ const blocks = [{ type: "paragraph", content: [{ type: "text", text: "Ciao", styles: {} }] }];
20
+
21
+ describe("BlogPostTranslation", () => {
22
+ it("BlogPostTranslation.rehydrate exposes source, aiStatus and translatedAt as Date", () => {
23
+ const translation = new BlogPostTranslation().rehydrate({
24
+ jsonApi: {
25
+ id: "t",
26
+ type: "blog-post-translations",
27
+ attributes: {
28
+ language: "it",
29
+ isOriginal: false,
30
+ source: "manual",
31
+ aiStatus: "completed",
32
+ title: "Titolo",
33
+ slug: "titolo",
34
+ body: JSON.stringify(blocks),
35
+ readingMinutes: 2,
36
+ translatedAt: "2026-10-12T09:00:00.000Z",
37
+ },
38
+ },
39
+ included: [],
40
+ } as any);
41
+
42
+ expect(translation.language).toBe("it");
43
+ expect(translation.isOriginal).toBe(false);
44
+ expect(translation.source).toBe("manual");
45
+ expect(translation.aiStatus).toBe("completed");
46
+ expect(translation.translatedAt).toBeInstanceOf(Date);
47
+ expect(translation.translatedAt?.toISOString()).toBe("2026-10-12T09:00:00.000Z");
48
+ expect(translation.body).toEqual(blocks);
49
+ expect(translation.readingMinutes).toBe(2);
50
+ });
51
+
52
+ it("a pending placeholder has no text and no translatedAt", () => {
53
+ const translation = new BlogPostTranslation().rehydrate({
54
+ jsonApi: {
55
+ id: "t",
56
+ type: "blog-post-translations",
57
+ attributes: { language: "it", isOriginal: false, source: "ai", aiStatus: "pending" },
58
+ },
59
+ included: [],
60
+ } as any);
61
+
62
+ expect(translation.aiStatus).toBe("pending");
63
+ expect(translation.title).toBe("");
64
+ expect(translation.body).toEqual([]);
65
+ expect(translation.translatedAt).toBeUndefined();
66
+ });
67
+
68
+ it("createJsonApi stringifies the body and sends only the editable fields", () => {
69
+ const payload = new BlogPostTranslation().createJsonApi({ id: "t", title: "Titolo", body: blocks });
70
+ expect(payload.data.type).toBe("blog-post-translations");
71
+ expect(payload.data.attributes).toEqual({ title: "Titolo", body: JSON.stringify(blocks) });
72
+ });
73
+ });
@@ -0,0 +1,110 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
2
+ import { BlogAiStatus, BlogTranslationSource } from "../types/blog.constants";
3
+ import { BlogPostInterface } from "./BlogPostInterface";
4
+ import { BlogPostTranslationInput, BlogPostTranslationInterface } from "./BlogPostTranslationInterface";
5
+
6
+ export class BlogPostTranslation extends AbstractApiData implements BlogPostTranslationInterface {
7
+ private _language?: string;
8
+ private _isOriginal?: boolean;
9
+ private _source?: BlogTranslationSource;
10
+ private _aiStatus?: BlogAiStatus;
11
+ private _title?: string;
12
+ private _slug?: string;
13
+ private _summary?: string;
14
+ private _body?: any;
15
+ private _coverImageAlt?: string;
16
+ private _readingMinutes?: number;
17
+ private _translatedAt?: Date;
18
+ private _post?: BlogPostInterface;
19
+
20
+ get language(): string {
21
+ return this._language ?? "";
22
+ }
23
+
24
+ get isOriginal(): boolean {
25
+ return this._isOriginal ?? false;
26
+ }
27
+
28
+ get source(): BlogTranslationSource {
29
+ return this._source ?? "ai";
30
+ }
31
+
32
+ get aiStatus(): BlogAiStatus {
33
+ return this._aiStatus ?? "pending";
34
+ }
35
+
36
+ get title(): string {
37
+ return this._title ?? "";
38
+ }
39
+
40
+ get slug(): string {
41
+ return this._slug ?? "";
42
+ }
43
+
44
+ get summary(): string | undefined {
45
+ return this._summary;
46
+ }
47
+
48
+ get body(): any {
49
+ return this._body ?? [];
50
+ }
51
+
52
+ get coverImageAlt(): string | undefined {
53
+ return this._coverImageAlt;
54
+ }
55
+
56
+ get readingMinutes(): number {
57
+ return this._readingMinutes ?? 1;
58
+ }
59
+
60
+ get translatedAt(): Date | undefined {
61
+ return this._translatedAt;
62
+ }
63
+
64
+ get post(): BlogPostInterface | undefined {
65
+ return this._post;
66
+ }
67
+
68
+ rehydrate(data: JsonApiHydratedDataInterface): this {
69
+ super.rehydrate(data);
70
+
71
+ const attributes = data.jsonApi.attributes ?? {};
72
+
73
+ this._language = attributes.language;
74
+ this._isOriginal = attributes.isOriginal === true;
75
+ this._source = attributes.source;
76
+ this._aiStatus = attributes.aiStatus;
77
+ this._title = attributes.title ?? undefined;
78
+ this._slug = attributes.slug ?? undefined;
79
+ this._summary = attributes.summary ?? undefined;
80
+ this._body = attributes.body ? JSON.parse(attributes.body) : undefined;
81
+ this._coverImageAlt = attributes.coverImageAlt ?? undefined;
82
+ this._readingMinutes = attributes.readingMinutes ?? undefined;
83
+ this._translatedAt = attributes.translatedAt ? new Date(attributes.translatedAt) : undefined;
84
+
85
+ this._post = this._readIncluded(data, "post", Modules.BlogPost) as BlogPostInterface;
86
+
87
+ return this;
88
+ }
89
+
90
+ createJsonApi(data: BlogPostTranslationInput) {
91
+ const response: any = {
92
+ data: {
93
+ type: Modules.BlogPostTranslation.name,
94
+ id: data.id,
95
+ attributes: {},
96
+ meta: {},
97
+ relationships: {},
98
+ },
99
+ included: [],
100
+ };
101
+
102
+ if (data.title !== undefined) response.data.attributes.title = data.title;
103
+ if (data.slug !== undefined) response.data.attributes.slug = data.slug;
104
+ if (data.summary !== undefined) response.data.attributes.summary = data.summary;
105
+ if (data.body !== undefined) response.data.attributes.body = JSON.stringify(data.body);
106
+ if (data.coverImageAlt !== undefined) response.data.attributes.coverImageAlt = data.coverImageAlt;
107
+
108
+ return response;
109
+ }
110
+ }
@@ -0,0 +1,27 @@
1
+ import { ApiDataInterface } from "../../../core";
2
+ import { BlogAiStatus, BlogTranslationSource } from "../types/blog.constants";
3
+ import { BlogPostInterface } from "./BlogPostInterface";
4
+
5
+ export type BlogPostTranslationInput = {
6
+ id: string;
7
+ title?: string;
8
+ slug?: string;
9
+ summary?: string;
10
+ body?: any;
11
+ coverImageAlt?: string;
12
+ };
13
+
14
+ export interface BlogPostTranslationInterface extends ApiDataInterface {
15
+ get language(): string;
16
+ get isOriginal(): boolean;
17
+ get source(): BlogTranslationSource;
18
+ get aiStatus(): BlogAiStatus;
19
+ get title(): string;
20
+ get slug(): string;
21
+ get summary(): string | undefined;
22
+ get body(): any;
23
+ get coverImageAlt(): string | undefined;
24
+ get readingMinutes(): number;
25
+ get translatedAt(): Date | undefined;
26
+ get post(): BlogPostInterface | undefined;
27
+ }
@@ -0,0 +1,30 @@
1
+ import { AbstractService, EndpointCreator, HttpMethod, Modules } from "../../../core";
2
+ import { BLOG_ACTION } from "../types/blog.constants";
3
+ import { BlogPostTranslationInput, BlogPostTranslationInterface } from "./BlogPostTranslationInterface";
4
+
5
+ export class BlogPostTranslationService extends AbstractService {
6
+ static async update(params: BlogPostTranslationInput): Promise<BlogPostTranslationInterface> {
7
+ return this.callApi<BlogPostTranslationInterface>({
8
+ type: Modules.BlogPostTranslation,
9
+ method: HttpMethod.PUT,
10
+ endpoint: new EndpointCreator({ endpoint: Modules.BlogPostTranslation, id: params.id }).generate(),
11
+ input: params,
12
+ });
13
+ }
14
+
15
+ /**
16
+ * POST blog-post-translations/:id/retranslate. Re-queues this one language
17
+ * from the current original. Replies 204.
18
+ */
19
+ static async retranslate(params: { id: string }): Promise<void> {
20
+ await this.callApi({
21
+ type: Modules.BlogPostTranslation,
22
+ method: HttpMethod.POST,
23
+ endpoint: new EndpointCreator({
24
+ endpoint: Modules.BlogPostTranslation,
25
+ id: params.id,
26
+ childEndpoint: BLOG_ACTION.retranslate,
27
+ }).generate(),
28
+ });
29
+ }
30
+ }
@@ -0,0 +1,37 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
2
+ import { BLOG_LIST_LAYOUTS, BlogListLayout } from "../types/blog.constants";
3
+ import { BlogSettingsInput, BlogSettingsInterface } from "./BlogSettingsInterface";
4
+
5
+ export class BlogSettings extends AbstractApiData implements BlogSettingsInterface {
6
+ private _listLayout?: BlogListLayout;
7
+
8
+ get listLayout(): BlogListLayout {
9
+ return this._listLayout ?? "featured";
10
+ }
11
+
12
+ rehydrate(data: JsonApiHydratedDataInterface): this {
13
+ super.rehydrate(data);
14
+
15
+ const listLayout = data.jsonApi.attributes?.listLayout;
16
+ this._listLayout = BLOG_LIST_LAYOUTS.includes(listLayout) ? listLayout : undefined;
17
+
18
+ return this;
19
+ }
20
+
21
+ createJsonApi(data: BlogSettingsInput) {
22
+ const response: any = {
23
+ data: {
24
+ type: Modules.BlogSettings.name,
25
+ id: data.id,
26
+ attributes: {},
27
+ meta: {},
28
+ relationships: {},
29
+ },
30
+ included: [],
31
+ };
32
+
33
+ response.data.attributes.listLayout = data.listLayout;
34
+
35
+ return response;
36
+ }
37
+ }
@@ -0,0 +1,11 @@
1
+ import { ApiDataInterface } from "../../../core";
2
+ import { BlogListLayout } from "../types/blog.constants";
3
+
4
+ export type BlogSettingsInput = {
5
+ id: string;
6
+ listLayout: BlogListLayout;
7
+ };
8
+
9
+ export interface BlogSettingsInterface extends ApiDataInterface {
10
+ get listLayout(): BlogListLayout;
11
+ }
@@ -0,0 +1,24 @@
1
+ import { AbstractService, EndpointCreator, HttpMethod, Modules } from "../../../core";
2
+ import { BlogSettingsInput, BlogSettingsInterface } from "./BlogSettingsInterface";
3
+
4
+ export class BlogSettingsService extends AbstractService {
5
+ /**
6
+ * GET blog-settings: the stored singleton, or the API's default.
7
+ */
8
+ static async get(): Promise<BlogSettingsInterface> {
9
+ return this.callApi<BlogSettingsInterface>({
10
+ type: Modules.BlogSettings,
11
+ method: HttpMethod.GET,
12
+ endpoint: new EndpointCreator({ endpoint: Modules.BlogSettings }).generate(),
13
+ });
14
+ }
15
+
16
+ static async save(params: BlogSettingsInput): Promise<BlogSettingsInterface> {
17
+ return this.callApi<BlogSettingsInterface>({
18
+ type: Modules.BlogSettings,
19
+ method: HttpMethod.PUT,
20
+ endpoint: new EndpointCreator({ endpoint: Modules.BlogSettings, id: params.id }).generate(),
21
+ input: params,
22
+ });
23
+ }
24
+ }
@@ -0,0 +1,17 @@
1
+ export * from "./BlogPostInterface";
2
+ export * from "./BlogPost";
3
+ export * from "./BlogPostService";
4
+ export * from "./BlogPostTranslationInterface";
5
+ export * from "./BlogPostTranslation";
6
+ export * from "./BlogPostTranslationService";
7
+ export * from "./BlogCategoryInterface";
8
+ export * from "./BlogCategory";
9
+ export * from "./BlogCategoryService";
10
+ export * from "./BlogCategoryTranslationInterface";
11
+ export * from "./BlogCategoryTranslation";
12
+ export * from "./BlogCategoryTranslationService";
13
+ export * from "./BlogSettingsInterface";
14
+ export * from "./BlogSettings";
15
+ export * from "./BlogSettingsService";
16
+ export * from "./BlogPostFields";
17
+ export * from "./BlogCategoryFields";
@@ -0,0 +1,3 @@
1
+ export * from "./useBlogPostTableStructure";
2
+ export * from "./useBlogCategoryTableStructure";
3
+ export * from "./useBlogCategoryNames";
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useState } from "react";
4
+ import { BlogCategoryService } from "../data";
5
+ import { blogCategoryAdminName } from "../components/admin/BlogTranslationStatus";
6
+
7
+ /**
8
+ * Category names for the admin views, in each category's original language.
9
+ *
10
+ * A post response includes its categories without their translations (the
11
+ * framework includes one level), so the name is not on the included record.
12
+ * The category list does include the translations: it is read once and the
13
+ * returned lookup maps a category id to its name ("" while loading).
14
+ */
15
+ export function useBlogCategoryNames(): (categoryId: string) => string {
16
+ const [names, setNames] = useState<Record<string, string>>({});
17
+
18
+ useEffect(() => {
19
+ let cancelled = false;
20
+ BlogCategoryService.findMany({ fetchAll: true })
21
+ .then((categories) => {
22
+ if (cancelled) return;
23
+ setNames(Object.fromEntries(categories.map((category) => [category.id, blogCategoryAdminName(category)])));
24
+ })
25
+ .catch(() => {
26
+ // The names are decoration on an admin page: a failed read leaves the ids unnamed.
27
+ });
28
+ return () => {
29
+ cancelled = true;
30
+ };
31
+ }, []);
32
+
33
+ return useCallback((categoryId: string) => names[categoryId] ?? "", [names]);
34
+ }
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ import { ColumnDef } from "@tanstack/react-table";
4
+ import { useTranslations } from "next-intl";
5
+ import { useMemo } from "react";
6
+ import { registerTableGenerator, TableContent, UseTableStructureHook } from "../../../hooks";
7
+ import { BlogCategoryEditor } from "../components/admin/BlogCategoryEditor";
8
+ import { blogCategoryAdminName } from "../components/admin/BlogTranslationStatus";
9
+ import { BlogCategoryFields, BlogCategoryInterface } from "../data";
10
+
11
+ /**
12
+ * Admin categories table: the name (original language) opens BlogCategoryEditor
13
+ * for that row; "languages ready" counts completed translations. Categories have
14
+ * no page of their own. Mirrors hooks/useEmailAutomationTableStructure.tsx in
15
+ * features/email-automation.
16
+ */
17
+ export const useBlogCategoryTableStructure: UseTableStructureHook<BlogCategoryInterface, BlogCategoryFields> = (
18
+ params,
19
+ ) => {
20
+ const t = useTranslations();
21
+ const refresh = params.dataRetriever?.refresh;
22
+
23
+ const tableData = useMemo(() => {
24
+ return params.data.map((category: BlogCategoryInterface) => {
25
+ const entry: TableContent<BlogCategoryInterface> = { jsonApiData: category };
26
+ params.fields.forEach((field) => {
27
+ entry[field] = (category as any)[field as keyof BlogCategoryInterface];
28
+ });
29
+ return entry;
30
+ });
31
+ }, [params.data, params.fields]);
32
+
33
+ const fieldColumnMap: Partial<Record<BlogCategoryFields, () => any>> = {
34
+ [BlogCategoryFields.name]: () => ({
35
+ id: BlogCategoryFields.name,
36
+ accessorKey: BlogCategoryFields.name,
37
+ header: t("blog.fields.name"),
38
+ cell: ({ row }: { row: TableContent<BlogCategoryInterface> }) => {
39
+ const category: BlogCategoryInterface = row.original.jsonApiData;
40
+ return (
41
+ <BlogCategoryEditor
42
+ category={category}
43
+ onSuccess={refresh}
44
+ trigger={
45
+ <button type="button" className="text-primary text-start font-medium">
46
+ {blogCategoryAdminName(category) || category.id}
47
+ </button>
48
+ }
49
+ />
50
+ );
51
+ },
52
+ enableSorting: false,
53
+ enableHiding: false,
54
+ }),
55
+ [BlogCategoryFields.languages]: () => ({
56
+ id: BlogCategoryFields.languages,
57
+ accessorKey: BlogCategoryFields.languages,
58
+ header: t("blog.fields.languages"),
59
+ cell: ({ row }: { row: TableContent<BlogCategoryInterface> }) => {
60
+ const category: BlogCategoryInterface = row.original.jsonApiData;
61
+ const total = category.translations.length;
62
+ if (total === 0) return <span className="text-muted-foreground text-xs">—</span>;
63
+ const ready = category.translations.filter((translation) => translation.aiStatus === "completed").length;
64
+ return (
65
+ <span className="block text-xs tabular-nums text-end">
66
+ {ready}/{total}
67
+ </span>
68
+ );
69
+ },
70
+ enableSorting: false,
71
+ enableHiding: false,
72
+ }),
73
+ };
74
+
75
+ const columns = useMemo(() => {
76
+ return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
77
+ TableContent<BlogCategoryInterface>
78
+ >[];
79
+ }, [params.fields, fieldColumnMap, t, refresh]);
80
+
81
+ return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
82
+ };
83
+
84
+ registerTableGenerator("blog-categories", useBlogCategoryTableStructure);