@carlonicora/nextjs-jsonapi 5.3.0 → 5.5.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 (233) hide show
  1. package/dist/{BlockNoteEditor-WTSQANKI.mjs → BlockNoteEditor-GVQCKRMU.mjs} +9 -8
  2. package/dist/BlockNoteEditor-GVQCKRMU.mjs.map +1 -0
  3. package/dist/{BlockNoteEditor-UJ6363LC.js → BlockNoteEditor-THBJH2UR.js} +24 -23
  4. package/dist/BlockNoteEditor-THBJH2UR.js.map +1 -0
  5. package/dist/{BlockNoteViewer-JVKWJZ6J.mjs → BlockNoteViewer-U6E3DS4F.mjs} +3 -3
  6. package/dist/{BlockNoteViewer-EL4JFB6P.js → BlockNoteViewer-VZOVKTFO.js} +4 -4
  7. package/dist/{BlockNoteViewer-EL4JFB6P.js.map → BlockNoteViewer-VZOVKTFO.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 +442 -442
  13. package/dist/billing/index.mjs +4 -4
  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-3CGROPF3.mjs +107 -0
  17. package/dist/chunk-3CGROPF3.mjs.map +1 -0
  18. package/dist/{chunk-QKZUE5OF.mjs → chunk-3YW35YHQ.mjs} +1 -101
  19. package/dist/chunk-3YW35YHQ.mjs.map +1 -0
  20. package/dist/{chunk-6WU74VFU.mjs → chunk-6SQYXUVP.mjs} +2 -2
  21. package/dist/{chunk-CRI4T47Y.js → chunk-G6UE6MEP.js} +814 -59
  22. package/dist/chunk-G6UE6MEP.js.map +1 -0
  23. package/dist/{chunk-XGTQWBDJ.js → chunk-KSYQEKJX.js} +11 -111
  24. package/dist/chunk-KSYQEKJX.js.map +1 -0
  25. package/dist/{chunk-EO75CV76.js → chunk-S4YJ5WCW.js} +8 -8
  26. package/dist/{chunk-EO75CV76.js.map → chunk-S4YJ5WCW.js.map} +1 -1
  27. package/dist/{chunk-3BAM23VA.js → chunk-TY5FYP3G.js} +930 -883
  28. package/dist/chunk-TY5FYP3G.js.map +1 -0
  29. package/dist/{chunk-JQWI62NV.mjs → chunk-VC5A3AUZ.mjs} +776 -21
  30. package/dist/chunk-VC5A3AUZ.mjs.map +1 -0
  31. package/dist/{chunk-FNTQQFLH.mjs → chunk-W7F5BDSV.mjs} +2 -2
  32. package/dist/{chunk-QQQE3N2X.js → chunk-WPYT4UKJ.js} +3 -3
  33. package/dist/{chunk-QQQE3N2X.js.map → chunk-WPYT4UKJ.js.map} +1 -1
  34. package/dist/{chunk-MIZTWBPX.mjs → chunk-X5WIUUNG.mjs} +1485 -1438
  35. package/dist/chunk-X5WIUUNG.mjs.map +1 -0
  36. package/dist/chunk-YDYVJM4I.js +107 -0
  37. package/dist/chunk-YDYVJM4I.js.map +1 -0
  38. package/dist/client/index.js +5 -5
  39. package/dist/client/index.js.map +1 -1
  40. package/dist/client/index.mjs +4 -4
  41. package/dist/components/editors/server.d.mts +15 -0
  42. package/dist/components/editors/server.d.ts +15 -0
  43. package/dist/components/editors/server.js +38 -0
  44. package/dist/components/editors/server.js.map +1 -0
  45. package/dist/components/editors/server.mjs +38 -0
  46. package/dist/components/editors/server.mjs.map +1 -0
  47. package/dist/components/index.d.mts +46 -12
  48. package/dist/components/index.d.ts +46 -12
  49. package/dist/components/index.js +7 -5
  50. package/dist/components/index.js.map +1 -1
  51. package/dist/components/index.mjs +8 -6
  52. package/dist/contexts/index.d.mts +9 -1
  53. package/dist/contexts/index.d.ts +9 -1
  54. package/dist/contexts/index.js +5 -5
  55. package/dist/contexts/index.mjs +4 -4
  56. package/dist/core/index.d.mts +214 -3
  57. package/dist/core/index.d.ts +214 -3
  58. package/dist/core/index.js +38 -4
  59. package/dist/core/index.js.map +1 -1
  60. package/dist/core/index.mjs +37 -3
  61. package/dist/email-automations/index.d.mts +3 -0
  62. package/dist/email-automations/index.d.ts +3 -0
  63. package/dist/email-automations/index.js +99 -95
  64. package/dist/email-automations/index.js.map +1 -1
  65. package/dist/email-automations/index.mjs +30 -26
  66. package/dist/email-automations/index.mjs.map +1 -1
  67. package/dist/features/analytics/index.d.mts +11 -4
  68. package/dist/features/analytics/index.d.ts +11 -4
  69. package/dist/features/analytics/index.js +127 -135
  70. package/dist/features/analytics/index.js.map +1 -1
  71. package/dist/features/analytics/index.mjs +16 -24
  72. package/dist/features/analytics/index.mjs.map +1 -1
  73. package/dist/features/analytics/server.d.mts +5 -2
  74. package/dist/features/analytics/server.d.ts +5 -2
  75. package/dist/features/analytics/server.js +9 -8
  76. package/dist/features/analytics/server.js.map +1 -1
  77. package/dist/features/analytics/server.mjs +7 -6
  78. package/dist/features/analytics/server.mjs.map +1 -1
  79. package/dist/features/blog/index.css +30 -0
  80. package/dist/features/blog/index.css.map +1 -0
  81. package/dist/features/blog/index.d.mts +276 -0
  82. package/dist/features/blog/index.d.ts +276 -0
  83. package/dist/features/blog/index.js +1810 -0
  84. package/dist/features/blog/index.js.map +1 -0
  85. package/dist/features/blog/index.mjs +1810 -0
  86. package/dist/features/blog/index.mjs.map +1 -0
  87. package/dist/features/blog/server.d.mts +36 -0
  88. package/dist/features/blog/server.d.ts +36 -0
  89. package/dist/features/blog/server.js +167 -0
  90. package/dist/features/blog/server.js.map +1 -0
  91. package/dist/features/blog/server.mjs +167 -0
  92. package/dist/features/blog/server.mjs.map +1 -0
  93. package/dist/features/help/index.js +38 -38
  94. package/dist/features/help/index.mjs +4 -4
  95. package/dist/features/tokenusage/index.d.mts +1 -3
  96. package/dist/features/tokenusage/index.d.ts +1 -3
  97. package/dist/features/tokenusage/index.js +127 -135
  98. package/dist/features/tokenusage/index.js.map +1 -1
  99. package/dist/features/tokenusage/index.mjs +10 -18
  100. package/dist/features/tokenusage/index.mjs.map +1 -1
  101. package/dist/index.d.mts +4 -2
  102. package/dist/index.d.ts +4 -2
  103. package/dist/index.js +39 -5
  104. package/dist/index.js.map +1 -1
  105. package/dist/index.mjs +38 -4
  106. package/dist/{s3.service-CY-jgOAM.d.ts → s3.service-Bz_K-tjz.d.ts} +11 -0
  107. package/dist/{s3.service-Sri0QP7Z.d.mts → s3.service-D5zNCEW3.d.mts} +11 -0
  108. package/dist/server/index.d.mts +1 -1
  109. package/dist/server/index.d.ts +1 -1
  110. package/dist/server/index.js +9 -7
  111. package/dist/server/index.js.map +1 -1
  112. package/dist/server/index.mjs +13 -11
  113. package/messages/en.json +93 -0
  114. package/messages/it.json +93 -0
  115. package/package.json +19 -1
  116. package/src/components/editors/BlockNoteEditor.tsx +13 -3
  117. package/src/components/editors/server/BlockNoteServerContent.tsx +28 -0
  118. package/src/components/editors/server/__tests__/renderBlockNoteHTML.spec.ts +44 -0
  119. package/src/components/editors/server/renderBlockNoteHTML.ts +26 -0
  120. package/src/components/editors/server-entry.ts +7 -0
  121. package/src/components/forms/FormBlockNote.tsx +4 -0
  122. package/src/components/index.ts +1 -0
  123. package/src/components/navigations/ContentTitle.tsx +14 -3
  124. package/src/components/viewers/BlockNoteStaticViewer.tsx +44 -0
  125. package/src/core/index.ts +4 -0
  126. package/src/core/registry/ModuleRegistry.ts +6 -0
  127. package/src/features/administration/components/AdminIndexContainer.tsx +61 -33
  128. package/src/features/administration/components/__tests__/AdminIndexContainer.spec.tsx +61 -1
  129. package/src/features/administration/components/index.ts +2 -1
  130. package/src/features/administration/contexts/AdministrationContext.tsx +11 -3
  131. package/src/features/administration/contexts/__tests__/AdministrationContext.spec.tsx +40 -0
  132. package/src/features/ai-connection/ai-connection.module.ts +2 -0
  133. package/src/features/ai-connection/contexts/AiConnectionsContext.tsx +3 -16
  134. package/src/features/ai-connection/contexts/__tests__/AiConnectionsContext.spec.tsx +33 -0
  135. package/src/features/analytics/__tests__/server-entry.spec.tsx +2 -1
  136. package/src/features/analytics/analytics.modules.ts +22 -5
  137. package/src/features/analytics/components/AnalyticsAdminContainer.tsx +4 -3
  138. package/src/features/analytics/components/AnalyticsAdminPageContainer.tsx +6 -3
  139. package/src/features/analytics/components/AnalyticsJourneyContainer.tsx +4 -3
  140. package/src/features/analytics/components/__tests__/AnalyticsJourneyContainer.spec.tsx +11 -0
  141. package/src/features/analytics/components/__tests__/AnalyticsPageContainers.spec.tsx +12 -1
  142. package/src/features/analytics/components/server/AnalyticsAdminPage.tsx +6 -3
  143. package/src/features/analytics/contexts/AnalyticsAdminContext.tsx +8 -17
  144. package/src/features/analytics/contexts/__tests__/AnalyticsAdminContext.spec.tsx +17 -0
  145. package/src/features/billing/stripe-price/stripe-price.module.ts +2 -0
  146. package/src/features/billing/stripe-product/stripe-product.module.ts +2 -0
  147. package/src/features/blog/BlogModules.ts +49 -0
  148. package/src/features/blog/components/admin/BlogAdminContainer.tsx +39 -0
  149. package/src/features/blog/components/admin/BlogCategoriesList.tsx +56 -0
  150. package/src/features/blog/components/admin/BlogCategoryEditor.tsx +230 -0
  151. package/src/features/blog/components/admin/BlogCoverImageField.tsx +123 -0
  152. package/src/features/blog/components/admin/BlogLayoutSetting.tsx +86 -0
  153. package/src/features/blog/components/admin/BlogPostComparison.tsx +54 -0
  154. package/src/features/blog/components/admin/BlogPostContainer.tsx +101 -0
  155. package/src/features/blog/components/admin/BlogPostDeleter.tsx +35 -0
  156. package/src/features/blog/components/admin/BlogPostEditor.tsx +216 -0
  157. package/src/features/blog/components/admin/BlogPostPublishButton.tsx +50 -0
  158. package/src/features/blog/components/admin/BlogPostTranslationEditor.tsx +118 -0
  159. package/src/features/blog/components/admin/BlogPostTranslationPanel.tsx +136 -0
  160. package/src/features/blog/components/admin/BlogPostsList.tsx +58 -0
  161. package/src/features/blog/components/admin/BlogTranslationStatus.tsx +104 -0
  162. package/src/features/blog/components/admin/__tests__/BlogPostTranslationPanel.spec.tsx +100 -0
  163. package/src/features/blog/components/admin/__tests__/BlogPostsList.spec.tsx +30 -0
  164. package/src/features/blog/components/admin/index.ts +14 -0
  165. package/src/features/blog/components/public/BlogCategoryChips.tsx +59 -0
  166. package/src/features/blog/components/public/BlogList.tsx +21 -0
  167. package/src/features/blog/components/public/BlogListColumn.tsx +53 -0
  168. package/src/features/blog/components/public/BlogListFeatured.tsx +57 -0
  169. package/src/features/blog/components/public/BlogListParts.tsx +74 -0
  170. package/src/features/blog/components/public/BlogListRail.tsx +62 -0
  171. package/src/features/blog/components/public/BlogPostCard.tsx +125 -0
  172. package/src/features/blog/components/public/BlogPostMeta.tsx +49 -0
  173. package/src/features/blog/components/public/BlogPostView.tsx +80 -0
  174. package/src/features/blog/components/public/BlogSearchBox.tsx +52 -0
  175. package/src/features/blog/components/public/__tests__/BlogList.spec.tsx +175 -0
  176. package/src/features/blog/components/public/__tests__/BlogPostView.spec.tsx +134 -0
  177. package/src/features/blog/components/public/blogPublicHelpers.ts +30 -0
  178. package/src/features/blog/components/public/index.ts +3 -0
  179. package/src/features/blog/contexts/BlogPostContext.tsx +66 -0
  180. package/src/features/blog/contexts/index.ts +1 -0
  181. package/src/features/blog/data/BlogCategory.ts +93 -0
  182. package/src/features/blog/data/BlogCategoryFields.ts +4 -0
  183. package/src/features/blog/data/BlogCategoryInterface.ts +24 -0
  184. package/src/features/blog/data/BlogCategoryService.ts +54 -0
  185. package/src/features/blog/data/BlogCategoryTranslation.ts +90 -0
  186. package/src/features/blog/data/BlogCategoryTranslationInterface.ts +22 -0
  187. package/src/features/blog/data/BlogCategoryTranslationService.ts +30 -0
  188. package/src/features/blog/data/BlogPost.spec.ts +123 -0
  189. package/src/features/blog/data/BlogPost.ts +159 -0
  190. package/src/features/blog/data/BlogPostFields.ts +7 -0
  191. package/src/features/blog/data/BlogPostInterface.ts +39 -0
  192. package/src/features/blog/data/BlogPostService.ts +82 -0
  193. package/src/features/blog/data/BlogPostTranslation.spec.ts +73 -0
  194. package/src/features/blog/data/BlogPostTranslation.ts +110 -0
  195. package/src/features/blog/data/BlogPostTranslationInterface.ts +27 -0
  196. package/src/features/blog/data/BlogPostTranslationService.ts +30 -0
  197. package/src/features/blog/data/BlogSettings.ts +37 -0
  198. package/src/features/blog/data/BlogSettingsInterface.ts +11 -0
  199. package/src/features/blog/data/BlogSettingsService.ts +24 -0
  200. package/src/features/blog/data/index.ts +17 -0
  201. package/src/features/blog/hooks/index.ts +3 -0
  202. package/src/features/blog/hooks/useBlogCategoryNames.ts +34 -0
  203. package/src/features/blog/hooks/useBlogCategoryTableStructure.tsx +84 -0
  204. package/src/features/blog/hooks/useBlogPostTableStructure.tsx +126 -0
  205. package/src/features/blog/index.ts +15 -0
  206. package/src/features/blog/server/fetchers.ts +169 -0
  207. package/src/features/blog/server/toPublic.spec.ts +91 -0
  208. package/src/features/blog/server/toPublic.ts +36 -0
  209. package/src/features/blog/server-entry.ts +17 -0
  210. package/src/features/blog/types/blog-public.types.ts +34 -0
  211. package/src/features/blog/types/blog.constants.ts +40 -0
  212. package/src/features/email-automation/components/containers/EmailAutomationsListContainer.tsx +12 -3
  213. package/src/features/rbac/contexts/RbacContext.tsx +4 -7
  214. package/src/features/rbac/contexts/__tests__/RbacContext.spec.tsx +42 -0
  215. package/src/features/s3/data/s3.service.ts +17 -0
  216. package/src/features/tokenusage/components/TokenUsageAdminContainer.tsx +4 -3
  217. package/src/features/tokenusage/contexts/TokenUsageAdminContext.tsx +3 -18
  218. package/src/features/tokenusage/contexts/__tests__/TokenUsageAdminContext.spec.tsx +17 -0
  219. package/src/features/tokenusage/tokenusage-admin.module.ts +4 -3
  220. package/src/features/tokenusage/tokenusage.modules.ts +12 -2
  221. package/src/features/user/contexts/UserContext.tsx +18 -3
  222. package/src/features/user/contexts/__tests__/UserContext.spec.tsx +95 -0
  223. package/dist/BlockNoteEditor-UJ6363LC.js.map +0 -1
  224. package/dist/BlockNoteEditor-WTSQANKI.mjs.map +0 -1
  225. package/dist/chunk-3BAM23VA.js.map +0 -1
  226. package/dist/chunk-CRI4T47Y.js.map +0 -1
  227. package/dist/chunk-JQWI62NV.mjs.map +0 -1
  228. package/dist/chunk-MIZTWBPX.mjs.map +0 -1
  229. package/dist/chunk-QKZUE5OF.mjs.map +0 -1
  230. package/dist/chunk-XGTQWBDJ.js.map +0 -1
  231. /package/dist/{BlockNoteViewer-JVKWJZ6J.mjs.map → BlockNoteViewer-U6E3DS4F.mjs.map} +0 -0
  232. /package/dist/{chunk-6WU74VFU.mjs.map → chunk-6SQYXUVP.mjs.map} +0 -0
  233. /package/dist/{chunk-FNTQQFLH.mjs.map → chunk-W7F5BDSV.mjs.map} +0 -0
package/messages/it.json CHANGED
@@ -736,6 +736,99 @@
736
736
  }
737
737
  }
738
738
  },
739
+ "blog": {
740
+ "admin": {
741
+ "title": "Blog",
742
+ "newPost": "Nuovo articolo",
743
+ "editPost": "Modifica articolo",
744
+ "newCategory": "Nuova categoria",
745
+ "editCategory": "Modifica categoria",
746
+ "empty": "Nessun articolo. Scrivi il primo.",
747
+ "emptyCategories": "Nessuna categoria."
748
+ },
749
+ "tabs": {
750
+ "posts": "Articoli",
751
+ "categories": "Categorie",
752
+ "layout": "Impaginazione"
753
+ },
754
+ "fields": {
755
+ "title": "Titolo",
756
+ "slug": "Slug",
757
+ "slugHelp": "L'ultima parte dell'indirizzo dell'articolo. Cambiarlo rompe i link che puntano già all'articolo.",
758
+ "summary": "Sommario",
759
+ "summaryHelp": "Lascialo vuoto e lo scriverà l'AI a partire dal testo.",
760
+ "body": "Testo",
761
+ "coverImage": "Immagine di copertina",
762
+ "coverImageAlt": "Descrizione dell'immagine di copertina",
763
+ "categories": "Categorie",
764
+ "originalLanguage": "Lingua originale",
765
+ "name": "Nome",
766
+ "description": "Descrizione",
767
+ "image": "Immagine",
768
+ "status": "Stato",
769
+ "publishedAt": "Pubblicato",
770
+ "languages": "Lingue pronte"
771
+ },
772
+ "status": {
773
+ "draft": "Bozza",
774
+ "published": "Pubblicato",
775
+ "unpublished": "Ritirato"
776
+ },
777
+ "actions": {
778
+ "publish": "Pubblica",
779
+ "unpublish": "Ritira",
780
+ "edit": "Modifica",
781
+ "translateAgain": "Traduci di nuovo",
782
+ "compareWithOriginal": "Confronta con l'originale",
783
+ "hideComparison": "Nascondi confronto"
784
+ },
785
+ "translation": {
786
+ "original": "Originale",
787
+ "translatedFrom": "Lingua originale: {language}, tradotto il {date}",
788
+ "editedByHand": "Modificato a mano",
789
+ "overwriteWarning": "Modificare l'originale lo farà tradurre di nuovo e sostituirà queste modifiche.",
790
+ "translating": "Traduzione in corso…",
791
+ "failed": "Traduzione non riuscita",
792
+ "pending": "In attesa di traduzione"
793
+ },
794
+ "layout": {
795
+ "title": "Impaginazione dell'elenco pubblico",
796
+ "featured": "In evidenza",
797
+ "featuredDescription": "L'articolo più recente in grande, poi una griglia di schede su tre colonne.",
798
+ "column": "Colonna",
799
+ "columnDescription": "Una colonna di lettura stretta, una riga per articolo con l'immagine accanto al testo.",
800
+ "rail": "Barra laterale",
801
+ "railDescription": "Ricerca e categorie in una barra laterale, l'articolo più recente in grande, gli altri su due colonne.",
802
+ "saved": "Impaginazione salvata"
803
+ },
804
+ "public": {
805
+ "eyebrow": "Blog",
806
+ "title": "Note e guide",
807
+ "subtitle": "Idee, guide e novità dal team.",
808
+ "all": "Tutti",
809
+ "search": "Cerca",
810
+ "searchPlaceholder": "Cerca negli articoli",
811
+ "loadMore": "Carica altri",
812
+ "readingTime": "{minutes} min di lettura",
813
+ "updated": "Aggiornato il {date}",
814
+ "by": "Di {author}",
815
+ "back": "Tutti gli articoli",
816
+ "moreIn": "Altro in {category}",
817
+ "empty": "Ancora nessun articolo.",
818
+ "noResults": "Nessun articolo corrisponde alla ricerca."
819
+ },
820
+ "entities": {
821
+ "post": "Articolo",
822
+ "posts": "Articoli",
823
+ "category": "Categoria",
824
+ "categories": "Categorie"
825
+ },
826
+ "errors": {
827
+ "title": "Il titolo è obbligatorio",
828
+ "name": "Il nome è obbligatorio",
829
+ "language": "Scegli la lingua originale"
830
+ }
831
+ },
739
832
  "canvas": {
740
833
  "toolbar": {
741
834
  "fitView": "Adatta alla vista",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlonicora/nextjs-jsonapi",
3
- "version": "5.3.0",
3
+ "version": "5.5.0",
4
4
  "description": "Next.js JSON:API client with server/client support and caching",
5
5
  "author": "Carlo Nicora",
6
6
  "license": "GPL-3.0-or-later",
@@ -49,6 +49,11 @@
49
49
  "require": "./dist/contexts/index.js"
50
50
  },
51
51
  "./contexts/styles.css": "./dist/contexts/index.css",
52
+ "./blocknote/server": {
53
+ "types": "./dist/components/editors/server.d.ts",
54
+ "import": "./dist/components/editors/server.mjs",
55
+ "require": "./dist/components/editors/server.js"
56
+ },
52
57
  "./testing": {
53
58
  "types": "./dist/testing/index.d.ts",
54
59
  "import": "./dist/testing/index.mjs",
@@ -74,6 +79,16 @@
74
79
  "import": "./dist/features/help/server.mjs",
75
80
  "require": "./dist/features/help/server.js"
76
81
  },
82
+ "./blog": {
83
+ "types": "./dist/features/blog/index.d.ts",
84
+ "import": "./dist/features/blog/index.mjs",
85
+ "require": "./dist/features/blog/index.js"
86
+ },
87
+ "./blog/server": {
88
+ "types": "./dist/features/blog/server.d.ts",
89
+ "import": "./dist/features/blog/server.mjs",
90
+ "require": "./dist/features/blog/server.js"
91
+ },
77
92
  "./tokenusage": {
78
93
  "types": "./dist/features/tokenusage/index.d.ts",
79
94
  "import": "./dist/features/tokenusage/index.mjs",
@@ -154,6 +169,7 @@
154
169
  "@base-ui/react": "1.8.0",
155
170
  "@blocknote/core": "^0.55.0",
156
171
  "@blocknote/react": "^0.55.0",
172
+ "@blocknote/server-util": "^0.55.0",
157
173
  "@blocknote/shadcn": "^0.55.0",
158
174
  "@blocknote/xl-ai": "^0.55.0",
159
175
  "@dnd-kit/core": "^6.3.1",
@@ -203,6 +219,8 @@
203
219
  "tw-animate-css": "^1.4.0",
204
220
  "uuid": "^14.0.2",
205
221
  "vaul": "^1.1.2",
222
+ "y-prosemirror": "^1.3.7",
223
+ "y-protocols": "^1.0.6",
206
224
  "zod": "^4.6.5"
207
225
  },
208
226
  "devDependencies": {
@@ -78,6 +78,13 @@ export type BlockNoteEditorProps = {
78
78
  diffContent?: PartialBlock[];
79
79
  placeholder?: string;
80
80
  bordered?: boolean;
81
+ /**
82
+ * Storage folder for pasted images, used instead of the current company's
83
+ * `companies/<companyId>/<type>` folder: the image key becomes
84
+ * `<uploadKeyPrefix>/<id>/<file name>`. For platform content edited by users
85
+ * who belong to no company (the blog). Omitted: the company folder, as before.
86
+ */
87
+ uploadKeyPrefix?: string;
81
88
  inlineContentSpecs?: Record<string, any>;
82
89
  renderOverlays?: (editor: any) => React.ReactNode;
83
90
  enableMentions?: boolean;
@@ -405,6 +412,7 @@ export default function BlockNoteEditor({
405
412
  diffContent,
406
413
  placeholder,
407
414
  bordered,
415
+ uploadKeyPrefix,
408
416
  inlineContentSpecs,
409
417
  renderOverlays,
410
418
  enableMentions,
@@ -547,7 +555,7 @@ export default function BlockNoteEditor({
547
555
 
548
556
  const uploadImage = useCallback(
549
557
  async (file: File): Promise<string> => {
550
- if (!company) {
558
+ if (!company && !uploadKeyPrefix) {
551
559
  errorToast({
552
560
  title: t(`common.errors.upload`),
553
561
  error: t(`common.errors.upload_description`),
@@ -556,7 +564,9 @@ export default function BlockNoteEditor({
556
564
  }
557
565
 
558
566
  const fileType = file.type;
559
- const key = `companies/${company.id}/${type}/${id}/${file.name}`;
567
+ const key = uploadKeyPrefix
568
+ ? `${uploadKeyPrefix}/${id}/${file.name}`
569
+ : `companies/${company!.id}/${type}/${id}/${file.name}`;
560
570
 
561
571
  const s3: S3Interface = await S3Service.getPreSignedUrl({
562
572
  key: key,
@@ -577,7 +587,7 @@ export default function BlockNoteEditor({
577
587
 
578
588
  return signedImage.url;
579
589
  },
580
- [company, id, t],
590
+ [company, id, t, type, uploadKeyPrefix],
581
591
  );
582
592
 
583
593
  // Utility: Remove trailing empty blocks for read-only display
@@ -0,0 +1,28 @@
1
+ import type { PartialBlock } from "@blocknote/core";
2
+ import "@blocknote/shadcn/style.css";
3
+ // Imported through the package's own client subpath, like the analytics server
4
+ // pages: the built server entry must reach the viewer through the "use client"
5
+ // bundle, not inline it.
6
+ import { BlockNoteStaticViewer } from "@carlonicora/nextjs-jsonapi/components";
7
+ import { renderBlockNoteHTML } from "./renderBlockNoteHTML";
8
+
9
+ /**
10
+ * Read-only rich text rendered on the server, so the text is in the HTML that
11
+ * crawlers read. For public read-only pages only: everywhere else, display goes
12
+ * through BlockNoteEditorContainer.
13
+ */
14
+ export async function BlockNoteServerContent({
15
+ content,
16
+ size,
17
+ className,
18
+ }: {
19
+ content?: PartialBlock[];
20
+ size?: "default" | "sm";
21
+ className?: string;
22
+ }) {
23
+ if (!Array.isArray(content) || content.length === 0) return null;
24
+
25
+ const html = await renderBlockNoteHTML(content);
26
+
27
+ return <BlockNoteStaticViewer html={html} size={size} className={className} />;
28
+ }
@@ -0,0 +1,44 @@
1
+ // @vitest-environment node
2
+ // The server render runs in Node, where there is no window or document of our own:
3
+ // BlockNote's ServerBlockNoteEditor brings its JSDOM.
4
+ import type { PartialBlock } from "@blocknote/core";
5
+ import { describe, expect, it } from "vitest";
6
+ import { renderBlockNoteHTML } from "../renderBlockNoteHTML";
7
+
8
+ const blocks: PartialBlock[] = [
9
+ { type: "heading", props: { level: 2 }, content: "Prep situations" },
10
+ {
11
+ type: "paragraph",
12
+ content: [
13
+ { type: "text", text: "A scene that can go ", styles: {} },
14
+ { type: "text", text: "three ways", styles: { bold: true } },
15
+ ],
16
+ },
17
+ { type: "bulletListItem", content: "First clue" },
18
+ { type: "bulletListItem", content: "Second clue" },
19
+ {
20
+ type: "paragraph",
21
+ content: [{ type: "link", href: "https://example.com/guide", content: "Read the guide" }],
22
+ },
23
+ ];
24
+
25
+ describe("renderBlockNoteHTML", () => {
26
+ it("renders the blocks' text into HTML on the server", async () => {
27
+ const html = await renderBlockNoteHTML(blocks);
28
+
29
+ expect(html).toContain("Prep situations");
30
+ expect(html).toMatch(/<h2[^>]*>.*Prep situations.*<\/h2>/s);
31
+ expect(html).toMatch(/<strong[^>]*>three ways<\/strong>/);
32
+ expect(html).toContain("First clue");
33
+ expect(html).toContain("Second clue");
34
+ expect(html).toContain('href="https://example.com/guide"');
35
+ expect(html).toContain("Read the guide");
36
+ });
37
+
38
+ it("leaves the server globals as they were", async () => {
39
+ await Promise.all([renderBlockNoteHTML(blocks), renderBlockNoteHTML(blocks)]);
40
+
41
+ expect(typeof (globalThis as { window?: unknown }).window).toBe("undefined");
42
+ expect(typeof (globalThis as { document?: unknown }).document).toBe("undefined");
43
+ });
44
+ });
@@ -0,0 +1,26 @@
1
+ import type { PartialBlock } from "@blocknote/core";
2
+ import { ServerBlockNoteEditor } from "@blocknote/server-util";
3
+
4
+ // Default schema only, the same one BlockNoteViewer uses: the editor's mention
5
+ // and diff inline specs are React specs from "use client" modules (next/link,
6
+ // hover cards), so they cannot run here.
7
+ let editor: ServerBlockNoteEditor | undefined;
8
+
9
+ // ServerBlockNoteEditor swaps globalThis.window/document for its JSDOM around
10
+ // each call and restores them after an await. Two overlapping calls would
11
+ // restore each other's values and could leave the JSDOM window on the server's
12
+ // globals, so calls run one at a time.
13
+ let queue: Promise<unknown> = Promise.resolve();
14
+
15
+ /**
16
+ * Renders a BlockNote blocks array to the HTML the editor itself would render,
17
+ * through BlockNote's own server editor (`blocksToFullHTML`). Server only.
18
+ */
19
+ export function renderBlockNoteHTML(blocks: PartialBlock[]): Promise<string> {
20
+ const run = queue.then(() => {
21
+ editor ??= ServerBlockNoteEditor.create();
22
+ return editor.blocksToFullHTML(blocks);
23
+ });
24
+ queue = run.catch(() => undefined);
25
+ return run;
26
+ }
@@ -0,0 +1,7 @@
1
+ // Server-only barrel for BlockNote rendering.
2
+ // Consumed via `@carlonicora/nextjs-jsonapi/blocknote/server`. Must NOT receive a
3
+ // "use client" directive: it loads @blocknote/server-util (and its JSDOM), which
4
+ // must never reach a client bundle. The app lists @blocknote/server-util in
5
+ // `serverExternalPackages` so Node loads it instead of the bundler.
6
+
7
+ export { BlockNoteServerContent } from "./server/BlockNoteServerContent";
@@ -41,6 +41,7 @@ export function FormBlockNote({
41
41
  aiConfig,
42
42
  aiHandleRef,
43
43
  headerAction,
44
+ uploadKeyPrefix,
44
45
  }: {
45
46
  form: any;
46
47
  id: string;
@@ -74,6 +75,8 @@ export function FormBlockNote({
74
75
  aiHandleRef?: React.MutableRefObject<import("../editors/BlockNoteEditor").BlockNoteAiHandle | null>;
75
76
  /** Rendered on the label row, right-aligned — e.g. a "Suggest impression" button. */
76
77
  headerAction?: React.ReactNode;
78
+ /** See `BlockNoteEditorProps.uploadKeyPrefix`. */
79
+ uploadKeyPrefix?: string;
77
80
  }) {
78
81
  const initialContentRef = useRef<any>(null);
79
82
  const lastEditorContentRef = useRef<any>(undefined);
@@ -133,6 +136,7 @@ export function FormBlockNote({
133
136
  bordered
134
137
  inlineContentSpecs={inlineContentSpecs}
135
138
  renderOverlays={renderOverlays}
139
+ uploadKeyPrefix={uploadKeyPrefix}
136
140
  enableMentions={enableMentions}
137
141
  mentionSearchFn={mentionSearchFn}
138
142
  mentionSearchParams={mentionSearchParams}
@@ -7,6 +7,7 @@ export * from "./contents";
7
7
  export * from "./details";
8
8
  export * from "./editors";
9
9
  export { BlockNoteViewerContainer } from "./viewers/BlockNoteViewerContainer";
10
+ export { BlockNoteStaticViewer } from "./viewers/BlockNoteStaticViewer";
10
11
  export * from "./errors";
11
12
  export * from "./forms";
12
13
  export * from "./navigations";
@@ -11,9 +11,20 @@ type TitleProps = {
11
11
  className?: string;
12
12
  module?: ModuleWithPermissions;
13
13
  prioritizeFunctions?: boolean;
14
+ /** Renders the title line as a real heading (public SEO pages). Omitted: a plain div, as before. */
15
+ headingLevel?: 1 | 2;
14
16
  };
15
17
 
16
- export function ContentTitle({ module, type, element, functions, className, prioritizeFunctions }: TitleProps) {
18
+ export function ContentTitle({
19
+ module,
20
+ type,
21
+ element,
22
+ functions,
23
+ className,
24
+ prioritizeFunctions,
25
+ headingLevel,
26
+ }: TitleProps) {
27
+ const ElementTag = headingLevel === 1 ? "h1" : headingLevel === 2 ? "h2" : "div";
17
28
  const [clientFunctions, setClientFunctions] = useState<ReactNode>(null);
18
29
  const [isClient, setIsClient] = useState(false);
19
30
 
@@ -43,9 +54,9 @@ export function ContentTitle({ module, type, element, functions, className, prio
43
54
  </div>
44
55
  )}
45
56
  {/* Element (name) - truncates with ellipsis when space is limited */}
46
- <div className={cn("text-primary text-3xl font-semibold", prioritizeFunctions ? "truncate" : "w-full")}>
57
+ <ElementTag className={cn("text-primary text-3xl font-semibold", prioritizeFunctions ? "truncate" : "w-full")}>
47
58
  {element}
48
- </div>
59
+ </ElementTag>
49
60
  </div>
50
61
  {/* Functions section - doesn't shrink when prioritizeFunctions is true */}
51
62
  {isClient && clientFunctions && (
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ import { useTheme } from "next-themes";
4
+ import { useEffect, useState } from "react";
5
+ import { cn } from "../../utils";
6
+
7
+ /**
8
+ * Shows HTML produced by BlockNoteServerContent inside the same container
9
+ * markup BlockNoteView renders, so BlockNote's stylesheet and the app's
10
+ * `.BlockNoteView` rules apply as they do to BlockNoteViewer.
11
+ */
12
+ export function BlockNoteStaticViewer({
13
+ html,
14
+ size,
15
+ className,
16
+ }: {
17
+ html: string;
18
+ size?: "default" | "sm";
19
+ className?: string;
20
+ }) {
21
+ // The server cannot know the theme: render light until mounted, then follow
22
+ // the app theme like BlockNoteViewer, so hydration never mismatches.
23
+ const [mounted, setMounted] = useState(false);
24
+ const { resolvedTheme } = useTheme();
25
+
26
+ useEffect(() => {
27
+ setMounted(true);
28
+ }, []);
29
+
30
+ const theme = mounted && resolvedTheme === "dark" ? "dark" : "light";
31
+
32
+ return (
33
+ <div
34
+ className={cn("bn-root bn-container", theme, "bn-shadcn BlockNoteView", size === "sm" && "small", className)}
35
+ data-color-scheme={theme}
36
+ >
37
+ <div
38
+ className="bn-editor bn-default-styles ProseMirror"
39
+ contentEditable={false}
40
+ dangerouslySetInnerHTML={{ __html: html }}
41
+ />
42
+ </div>
43
+ );
44
+ }
package/src/core/index.ts CHANGED
@@ -75,6 +75,10 @@ export * from "../features/email-automation/EmailAutomationTriggerModule";
75
75
  // server page can wrap EmailAutomationService.findOne in cache(). The client-marked
76
76
  // ./email-automations entry carries only components, contexts and hooks.
77
77
  export * from "../features/email-automation/data";
78
+ // Blog modules and data: server-safe, same placement as email automation above.
79
+ // The client-marked ./blog entry carries only components, contexts and hooks.
80
+ export * from "../features/blog/BlogModules";
81
+ export * from "../features/blog/data";
78
82
  export * from "../features/handbook/HandbookPageModule";
79
83
  export * from "../features/handbook/HandbookSectionModule";
80
84
  export * from "../features/handbook/HandbookThreadMessageModule";
@@ -46,6 +46,12 @@ export interface FoundationModuleDefinitions {
46
46
  EmailAutomationStep: ModuleWithPermissions;
47
47
  EmailAutomationEnrollment: ModuleWithPermissions;
48
48
  EmailAutomationTrigger: ModuleWithPermissions;
49
+ // Blog modules
50
+ BlogPost: ModuleWithPermissions;
51
+ BlogPostTranslation: ModuleWithPermissions;
52
+ BlogCategory: ModuleWithPermissions;
53
+ BlogCategoryTranslation: ModuleWithPermissions;
54
+ BlogSettings: ModuleWithPermissions;
49
55
  // Referral modules
50
56
  Referral: ModuleWithPermissions;
51
57
  ReferralStats: ModuleWithPermissions;
@@ -8,14 +8,21 @@ import { Link } from "../../../shadcnui";
8
8
  import { AdministrationProvider } from "../contexts/AdministrationContext";
9
9
  import type { AdminGroup, AdminSection } from "../data/admin-section.types";
10
10
 
11
+ /** Keys of the built-in platform sections, for `hiddenSections`. */
12
+ export type PlatformSectionKey = "companies" | "users" | "token-usage" | "ai-connections" | "rbac" | "billing";
13
+
11
14
  /**
12
- * The sections every NJA project has. Hrefs are constants rather than props:
13
- * the path convention IS the contract that makes this component reusable, and
14
- * it already exists in the package (see TOKEN_USAGE_ADMIN_PAGE_URL in
15
- * features/tokenusage/contexts/TokenUsageAdminContext.tsx).
15
+ * The sections every NJA project has, by key. Hrefs are constants rather than
16
+ * props: the path convention IS the contract that makes this component
17
+ * reusable. Exported so an app that passes its own `groups` places these
18
+ * entries without retyping their keys, icons and copy.
19
+ *
20
+ * `billing` is the only conditional entry: the default platform group shows it
21
+ * only when Stripe is configured (see AdminIndexGrid). An app that places it in
22
+ * its own `groups` applies the same check itself.
16
23
  */
17
- const PLATFORM_SECTIONS: AdminSection[] = [
18
- {
24
+ export const ADMIN_PLATFORM_SECTIONS: Record<PlatformSectionKey, AdminSection> = {
25
+ companies: {
19
26
  key: "companies",
20
27
  icon: Building2Icon,
21
28
  labelKey: "entities.companies",
@@ -23,7 +30,7 @@ const PLATFORM_SECTIONS: AdminSection[] = [
23
30
  descriptionKey: "administration.companies.description",
24
31
  href: "/administration/companies",
25
32
  },
26
- {
33
+ users: {
27
34
  key: "users",
28
35
  icon: UsersIcon,
29
36
  labelKey: "entities.users",
@@ -31,21 +38,21 @@ const PLATFORM_SECTIONS: AdminSection[] = [
31
38
  descriptionKey: "administration.users.description",
32
39
  href: "/administration/users",
33
40
  },
34
- {
41
+ "token-usage": {
35
42
  key: "token-usage",
36
43
  icon: CoinsIcon,
37
44
  labelKey: "token_usage.admin.title",
38
45
  descriptionKey: "administration.token_usage.description",
39
46
  href: "/administration/token-usage",
40
47
  },
41
- {
48
+ "ai-connections": {
42
49
  key: "ai-connections",
43
50
  icon: PlugZapIcon,
44
51
  labelKey: "ai_connections.admin.title",
45
52
  descriptionKey: "administration.ai_connections.description",
46
53
  href: "/administration/ai-connections",
47
54
  },
48
- {
55
+ rbac: {
49
56
  key: "rbac",
50
57
  icon: ShieldCheckIcon,
51
58
  labelKey: "entities.rbac",
@@ -53,27 +60,33 @@ const PLATFORM_SECTIONS: AdminSection[] = [
53
60
  descriptionKey: "administration.rbac.description",
54
61
  href: "/administration/rbac",
55
62
  },
56
- ];
57
-
58
- /**
59
- * The only conditional entry. getStripePublishableKey comes from /client rather
60
- * than isStripeConfigured() from /billing on purpose: /billing exists to keep
61
- * Stripe.js out of the global bundle, and isStripeConfigured lives in
62
- * StripeProvider.tsx, which imports @stripe/stripe-js at module scope. The two
63
- * are the same check — isStripeConfigured is a one-line wrapper over this call.
64
- */
65
- const BILLING_SECTION: AdminSection = {
66
- key: "billing",
67
- icon: CreditCardIcon,
68
- labelKey: "billing.admin.products.title",
69
- descriptionKey: "administration.billing.description",
70
- href: "/administration/products",
63
+ billing: {
64
+ key: "billing",
65
+ icon: CreditCardIcon,
66
+ labelKey: "billing.admin.products.title",
67
+ descriptionKey: "administration.billing.description",
68
+ href: "/administration/products",
69
+ },
71
70
  };
72
71
 
73
- /** Keys of the built-in platform sections, for `hiddenSections`. */
74
- export type PlatformSectionKey = "companies" | "users" | "token-usage" | "ai-connections" | "rbac" | "billing";
72
+ /** The unconditional platform entries, in the default group's order. */
73
+ const PLATFORM_SECTIONS: AdminSection[] = [
74
+ ADMIN_PLATFORM_SECTIONS.companies,
75
+ ADMIN_PLATFORM_SECTIONS.users,
76
+ ADMIN_PLATFORM_SECTIONS["token-usage"],
77
+ ADMIN_PLATFORM_SECTIONS["ai-connections"],
78
+ ADMIN_PLATFORM_SECTIONS.rbac,
79
+ ];
75
80
 
76
81
  type AdminIndexProps = {
82
+ /**
83
+ * Replaces the default groups (the built-in platform group plus
84
+ * `additionalGroups`) with exactly these, in this order. For an app that
85
+ * arranges its administration by its own headings; it places the platform
86
+ * entries it wants from ADMIN_PLATFORM_SECTIONS. When set, `additionalGroups`
87
+ * is ignored and the Stripe check on `billing` is the app's to apply.
88
+ */
89
+ groups?: AdminGroup[];
77
90
  /** Project-specific groups, appended after the built-in platform group. */
78
91
  additionalGroups?: AdminGroup[];
79
92
  /**
@@ -90,10 +103,12 @@ type AdminIndexProps = {
90
103
  * The grid itself, with no page shell — this is what the specs render, because
91
104
  * RoundPageContainer pulls in the app Header and its four header contexts.
92
105
  */
93
- export function AdminIndexGrid({ additionalGroups = [], hiddenSections = [] }: AdminIndexProps) {
106
+ export function AdminIndexGrid({ groups: customGroups, additionalGroups = [], hiddenSections = [] }: AdminIndexProps) {
94
107
  const t = useTranslations();
95
108
 
96
- const available = getStripePublishableKey() ? [...PLATFORM_SECTIONS, BILLING_SECTION] : PLATFORM_SECTIONS;
109
+ const available = getStripePublishableKey()
110
+ ? [...PLATFORM_SECTIONS, ADMIN_PLATFORM_SECTIONS.billing]
111
+ : PLATFORM_SECTIONS;
97
112
 
98
113
  const platformGroup: AdminGroup = {
99
114
  key: "platform",
@@ -101,7 +116,16 @@ export function AdminIndexGrid({ additionalGroups = [], hiddenSections = [] }: A
101
116
  sections: available.filter((section) => !hiddenSections.includes(section.key as PlatformSectionKey)),
102
117
  };
103
118
 
104
- const groups = [platformGroup, ...additionalGroups].filter((group) => group.sections.length > 0);
119
+ // Custom groups still honour hiddenSections, so a project that cannot serve
120
+ // a platform route drops it the same way in either mode.
121
+ const groups = (
122
+ customGroups
123
+ ? customGroups.map((group) => ({
124
+ ...group,
125
+ sections: group.sections.filter((section) => !hiddenSections.includes(section.key as PlatformSectionKey)),
126
+ }))
127
+ : [platformGroup, ...additionalGroups]
128
+ ).filter((group) => group.sections.length > 0);
105
129
 
106
130
  return (
107
131
  <div className="mx-auto flex w-full max-w-5xl flex-1 flex-col gap-8 py-8">
@@ -146,11 +170,15 @@ export function AdminIndexGrid({ additionalGroups = [], hiddenSections = [] }: A
146
170
  * and its own page chrome (AdministrationProvider), so it does not inherit the
147
171
  * title and breadcrumb of whatever page rendered before it.
148
172
  */
149
- export function AdminIndexContainer({ additionalGroups, hiddenSections }: AdminIndexProps) {
173
+ export function AdminIndexContainer({ groups, additionalGroups, hiddenSections }: AdminIndexProps) {
174
+ const t = useTranslations();
175
+
176
+ // The index is the "Administration" page itself, so it names itself in the
177
+ // breadcrumb like every administration page does ("Home > Companies").
150
178
  return (
151
- <AdministrationProvider>
179
+ <AdministrationProvider section={t("administration.title")}>
152
180
  <RoundPageContainer>
153
- <AdminIndexGrid additionalGroups={additionalGroups} hiddenSections={hiddenSections} />
181
+ <AdminIndexGrid groups={groups} additionalGroups={additionalGroups} hiddenSections={hiddenSections} />
154
182
  </RoundPageContainer>
155
183
  </AdministrationProvider>
156
184
  );