@carlonicora/nextjs-jsonapi 5.3.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 (173) hide show
  1. package/dist/{BlockNoteEditor-UJ6363LC.js → BlockNoteEditor-4YCAIEPT.js} +23 -22
  2. package/dist/BlockNoteEditor-4YCAIEPT.js.map +1 -0
  3. package/dist/{BlockNoteEditor-WTSQANKI.mjs → BlockNoteEditor-YQL2SGFR.mjs} +8 -7
  4. package/dist/BlockNoteEditor-YQL2SGFR.mjs.map +1 -0
  5. package/dist/{BlockNoteViewer-JVKWJZ6J.mjs → BlockNoteViewer-AEJKJ27Q.mjs} +2 -2
  6. package/dist/{BlockNoteViewer-EL4JFB6P.js → BlockNoteViewer-YL3J7J2Y.js} +3 -3
  7. package/dist/{BlockNoteViewer-EL4JFB6P.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-3BAM23VA.js → chunk-3NLAXVIN.js} +860 -849
  17. package/dist/chunk-3NLAXVIN.js.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-QQQE3N2X.js → chunk-5PPG6PL7.js} +3 -3
  21. package/dist/{chunk-QQQE3N2X.js.map → chunk-5PPG6PL7.js.map} +1 -1
  22. package/dist/{chunk-FNTQQFLH.mjs → chunk-6DC2WVVB.mjs} +2 -2
  23. package/dist/{chunk-MIZTWBPX.mjs → chunk-EUKTBOI2.mjs} +19 -8
  24. package/dist/chunk-EUKTBOI2.mjs.map +1 -0
  25. package/dist/{chunk-XGTQWBDJ.js → chunk-KSYQEKJX.js} +11 -111
  26. package/dist/chunk-KSYQEKJX.js.map +1 -0
  27. package/dist/{chunk-CRI4T47Y.js → chunk-NFBOSPNJ.js} +756 -41
  28. package/dist/chunk-NFBOSPNJ.js.map +1 -0
  29. package/dist/{chunk-6WU74VFU.mjs → chunk-QMYQRPQC.mjs} +2 -2
  30. package/dist/{chunk-JQWI62NV.mjs → chunk-WC63PJSC.mjs} +716 -1
  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-EO75CV76.js → chunk-YNCQSWQW.js} +8 -8
  35. package/dist/{chunk-EO75CV76.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.js +4 -4
  39. package/dist/client/index.mjs +3 -3
  40. package/dist/components/index.d.mts +13 -2
  41. package/dist/components/index.d.ts +13 -2
  42. package/dist/components/index.js +4 -4
  43. package/dist/components/index.mjs +3 -3
  44. package/dist/contexts/index.js +4 -4
  45. package/dist/contexts/index.mjs +3 -3
  46. package/dist/core/index.d.mts +214 -3
  47. package/dist/core/index.d.ts +214 -3
  48. package/dist/core/index.js +36 -2
  49. package/dist/core/index.js.map +1 -1
  50. package/dist/core/index.mjs +35 -1
  51. package/dist/email-automations/index.js +92 -92
  52. package/dist/email-automations/index.mjs +3 -3
  53. package/dist/features/analytics/index.js +122 -122
  54. package/dist/features/analytics/index.mjs +4 -4
  55. package/dist/features/analytics/server.js +6 -5
  56. package/dist/features/analytics/server.js.map +1 -1
  57. package/dist/features/analytics/server.mjs +4 -3
  58. package/dist/features/analytics/server.mjs.map +1 -1
  59. package/dist/features/blog/index.css +30 -0
  60. package/dist/features/blog/index.css.map +1 -0
  61. package/dist/features/blog/index.d.mts +266 -0
  62. package/dist/features/blog/index.d.ts +266 -0
  63. package/dist/features/blog/index.js +1799 -0
  64. package/dist/features/blog/index.js.map +1 -0
  65. package/dist/features/blog/index.mjs +1799 -0
  66. package/dist/features/blog/index.mjs.map +1 -0
  67. package/dist/features/blog/server.d.mts +33 -0
  68. package/dist/features/blog/server.d.ts +33 -0
  69. package/dist/features/blog/server.js +139 -0
  70. package/dist/features/blog/server.js.map +1 -0
  71. package/dist/features/blog/server.mjs +139 -0
  72. package/dist/features/blog/server.mjs.map +1 -0
  73. package/dist/features/help/index.js +37 -37
  74. package/dist/features/help/index.mjs +3 -3
  75. package/dist/features/tokenusage/index.js +125 -125
  76. package/dist/features/tokenusage/index.mjs +4 -4
  77. package/dist/index.d.mts +4 -2
  78. package/dist/index.d.ts +4 -2
  79. package/dist/index.js +37 -3
  80. package/dist/index.js.map +1 -1
  81. package/dist/index.mjs +36 -2
  82. package/dist/{s3.service-CY-jgOAM.d.ts → s3.service-Bz_K-tjz.d.ts} +11 -0
  83. package/dist/{s3.service-Sri0QP7Z.d.mts → s3.service-D5zNCEW3.d.mts} +11 -0
  84. package/dist/server/index.d.mts +1 -1
  85. package/dist/server/index.d.ts +1 -1
  86. package/dist/server/index.js +8 -6
  87. package/dist/server/index.js.map +1 -1
  88. package/dist/server/index.mjs +12 -10
  89. package/messages/en.json +93 -0
  90. package/messages/it.json +93 -0
  91. package/package.json +11 -1
  92. package/src/components/editors/BlockNoteEditor.tsx +13 -3
  93. package/src/components/forms/FormBlockNote.tsx +4 -0
  94. package/src/components/navigations/ContentTitle.tsx +14 -3
  95. package/src/core/index.ts +4 -0
  96. package/src/core/registry/ModuleRegistry.ts +6 -0
  97. package/src/features/blog/BlogModules.ts +49 -0
  98. package/src/features/blog/components/admin/BlogAdminContainer.tsx +32 -0
  99. package/src/features/blog/components/admin/BlogCategoriesList.tsx +56 -0
  100. package/src/features/blog/components/admin/BlogCategoryEditor.tsx +230 -0
  101. package/src/features/blog/components/admin/BlogCoverImageField.tsx +123 -0
  102. package/src/features/blog/components/admin/BlogLayoutSetting.tsx +86 -0
  103. package/src/features/blog/components/admin/BlogPostComparison.tsx +54 -0
  104. package/src/features/blog/components/admin/BlogPostContainer.tsx +102 -0
  105. package/src/features/blog/components/admin/BlogPostDeleter.tsx +35 -0
  106. package/src/features/blog/components/admin/BlogPostEditor.tsx +207 -0
  107. package/src/features/blog/components/admin/BlogPostPublishButton.tsx +50 -0
  108. package/src/features/blog/components/admin/BlogPostTranslationEditor.tsx +110 -0
  109. package/src/features/blog/components/admin/BlogPostTranslationPanel.tsx +136 -0
  110. package/src/features/blog/components/admin/BlogPostsList.tsx +58 -0
  111. package/src/features/blog/components/admin/BlogTranslationStatus.tsx +104 -0
  112. package/src/features/blog/components/admin/__tests__/BlogPostTranslationPanel.spec.tsx +100 -0
  113. package/src/features/blog/components/admin/__tests__/BlogPostsList.spec.tsx +30 -0
  114. package/src/features/blog/components/admin/index.ts +14 -0
  115. package/src/features/blog/components/public/BlogCategoryChips.tsx +59 -0
  116. package/src/features/blog/components/public/BlogList.tsx +21 -0
  117. package/src/features/blog/components/public/BlogListColumn.tsx +53 -0
  118. package/src/features/blog/components/public/BlogListFeatured.tsx +57 -0
  119. package/src/features/blog/components/public/BlogListParts.tsx +74 -0
  120. package/src/features/blog/components/public/BlogListRail.tsx +62 -0
  121. package/src/features/blog/components/public/BlogPostCard.tsx +132 -0
  122. package/src/features/blog/components/public/BlogPostMeta.tsx +49 -0
  123. package/src/features/blog/components/public/BlogPostView.tsx +74 -0
  124. package/src/features/blog/components/public/BlogSearchBox.tsx +52 -0
  125. package/src/features/blog/components/public/__tests__/BlogList.spec.tsx +173 -0
  126. package/src/features/blog/components/public/__tests__/BlogPostView.spec.tsx +128 -0
  127. package/src/features/blog/components/public/blogPublicHelpers.ts +30 -0
  128. package/src/features/blog/components/public/index.ts +3 -0
  129. package/src/features/blog/contexts/BlogPostContext.tsx +66 -0
  130. package/src/features/blog/contexts/index.ts +1 -0
  131. package/src/features/blog/data/BlogCategory.ts +93 -0
  132. package/src/features/blog/data/BlogCategoryFields.ts +4 -0
  133. package/src/features/blog/data/BlogCategoryInterface.ts +24 -0
  134. package/src/features/blog/data/BlogCategoryService.ts +54 -0
  135. package/src/features/blog/data/BlogCategoryTranslation.ts +90 -0
  136. package/src/features/blog/data/BlogCategoryTranslationInterface.ts +22 -0
  137. package/src/features/blog/data/BlogCategoryTranslationService.ts +30 -0
  138. package/src/features/blog/data/BlogPost.spec.ts +123 -0
  139. package/src/features/blog/data/BlogPost.ts +159 -0
  140. package/src/features/blog/data/BlogPostFields.ts +7 -0
  141. package/src/features/blog/data/BlogPostInterface.ts +39 -0
  142. package/src/features/blog/data/BlogPostService.ts +82 -0
  143. package/src/features/blog/data/BlogPostTranslation.spec.ts +73 -0
  144. package/src/features/blog/data/BlogPostTranslation.ts +110 -0
  145. package/src/features/blog/data/BlogPostTranslationInterface.ts +27 -0
  146. package/src/features/blog/data/BlogPostTranslationService.ts +30 -0
  147. package/src/features/blog/data/BlogSettings.ts +37 -0
  148. package/src/features/blog/data/BlogSettingsInterface.ts +11 -0
  149. package/src/features/blog/data/BlogSettingsService.ts +24 -0
  150. package/src/features/blog/data/index.ts +17 -0
  151. package/src/features/blog/hooks/index.ts +3 -0
  152. package/src/features/blog/hooks/useBlogCategoryNames.ts +34 -0
  153. package/src/features/blog/hooks/useBlogCategoryTableStructure.tsx +84 -0
  154. package/src/features/blog/hooks/useBlogPostTableStructure.tsx +126 -0
  155. package/src/features/blog/index.ts +15 -0
  156. package/src/features/blog/server/fetchers.ts +120 -0
  157. package/src/features/blog/server/toPublic.spec.ts +91 -0
  158. package/src/features/blog/server/toPublic.ts +36 -0
  159. package/src/features/blog/server-entry.ts +17 -0
  160. package/src/features/blog/types/blog-public.types.ts +34 -0
  161. package/src/features/blog/types/blog.constants.ts +40 -0
  162. package/src/features/s3/data/s3.service.ts +17 -0
  163. package/dist/BlockNoteEditor-UJ6363LC.js.map +0 -1
  164. package/dist/BlockNoteEditor-WTSQANKI.mjs.map +0 -1
  165. package/dist/chunk-3BAM23VA.js.map +0 -1
  166. package/dist/chunk-CRI4T47Y.js.map +0 -1
  167. package/dist/chunk-JQWI62NV.mjs.map +0 -1
  168. package/dist/chunk-MIZTWBPX.mjs.map +0 -1
  169. package/dist/chunk-QKZUE5OF.mjs.map +0 -1
  170. package/dist/chunk-XGTQWBDJ.js.map +0 -1
  171. /package/dist/{BlockNoteViewer-JVKWJZ6J.mjs.map → BlockNoteViewer-AEJKJ27Q.mjs.map} +0 -0
  172. /package/dist/{chunk-FNTQQFLH.mjs.map → chunk-6DC2WVVB.mjs.map} +0 -0
  173. /package/dist/{chunk-6WU74VFU.mjs.map → chunk-QMYQRPQC.mjs.map} +0 -0
@@ -0,0 +1,1799 @@
1
+ "use client";
2
+ import {
3
+ AdministrationProvider,
4
+ Alert,
5
+ AlertDescription,
6
+ Badge,
7
+ BlockNoteEditorContainer,
8
+ Button,
9
+ Collapsible,
10
+ CollapsibleContent,
11
+ CollapsibleTrigger,
12
+ CommonDeleter,
13
+ ContentListTable,
14
+ ContentTitle,
15
+ DetailField,
16
+ EditorSheet,
17
+ EmptyState,
18
+ EntityMultiSelector,
19
+ FileInput,
20
+ FileUploader,
21
+ FormBlockNote,
22
+ FormFieldWrapper,
23
+ FormInput,
24
+ FormSelect,
25
+ FormTextarea,
26
+ InputGroup,
27
+ InputGroupAddon,
28
+ InputGroupInput,
29
+ Link,
30
+ MicroLabel,
31
+ RadioGroup,
32
+ RadioGroupItem,
33
+ RoundPageContainer,
34
+ SectionHeader,
35
+ SharedProvider,
36
+ buttonVariants,
37
+ errorToast,
38
+ registerTableGenerator,
39
+ useDataListRetriever,
40
+ usePageUrlGenerator
41
+ } from "../../chunk-EUKTBOI2.mjs";
42
+ import "../../chunk-QMYQRPQC.mjs";
43
+ import "../../chunk-PW42LRSL.mjs";
44
+ import {
45
+ BLOG_ACTION,
46
+ BLOG_CATEGORY_UPLOAD_PREFIX,
47
+ BLOG_LIST_LAYOUTS,
48
+ BLOG_MODULE_ID,
49
+ BLOG_POST_STATUSES,
50
+ BLOG_POST_UPLOAD_PREFIX,
51
+ BLOG_PUBLIC_PREFIX,
52
+ BLOG_SETTINGS_ID,
53
+ BLOG_TRANSLATION_SOURCES,
54
+ BLOG_UPLOAD_ROOT,
55
+ BlogCategoryService,
56
+ BlogCategoryTranslationService,
57
+ BlogPostService,
58
+ BlogPostTranslationService,
59
+ BlogSettingsService,
60
+ Modules,
61
+ S3Service,
62
+ cn,
63
+ getI18nSupportedLocales,
64
+ isRemoteImageSrc,
65
+ showToast,
66
+ useI18nLocale,
67
+ useI18nRouter
68
+ } from "../../chunk-WC63PJSC.mjs";
69
+ import "../../chunk-AUXK7QSA.mjs";
70
+ import "../../chunk-PI56A4DG.mjs";
71
+ import "../../chunk-V5MI7RNB.mjs";
72
+ import "../../chunk-VOXD3ZLY.mjs";
73
+ import {
74
+ __name
75
+ } from "../../chunk-PAWJFY3S.mjs";
76
+
77
+ // src/features/blog/components/admin/BlogAdminContainer.tsx
78
+ import { useTranslations as useTranslations9 } from "next-intl";
79
+
80
+ // src/features/blog/components/admin/BlogCategoriesList.tsx
81
+ import { TagIcon } from "lucide-react";
82
+ import { useTranslations as useTranslations4 } from "next-intl";
83
+
84
+ // src/features/blog/hooks/useBlogCategoryTableStructure.tsx
85
+ import { useTranslations as useTranslations3 } from "next-intl";
86
+ import { useMemo as useMemo4 } from "react";
87
+
88
+ // src/features/blog/components/admin/BlogCategoryEditor.tsx
89
+ import { zodResolver } from "@hookform/resolvers/zod";
90
+ import { ChevronDownIcon } from "lucide-react";
91
+ import { useTranslations as useTranslations2 } from "next-intl";
92
+ import { useCallback as useCallback2, useMemo as useMemo3, useState as useState3 } from "react";
93
+ import { useForm } from "react-hook-form";
94
+ import { v4 } from "uuid";
95
+ import { z } from "zod";
96
+
97
+ // src/features/blog/components/admin/BlogCoverImageField.tsx
98
+ import { UploadIcon } from "lucide-react";
99
+ import Image from "next/image";
100
+ import { useEffect, useMemo, useState } from "react";
101
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
102
+ var dropzone = {
103
+ multiple: false,
104
+ maxSize: 100 * 1024 * 1024,
105
+ preventDropOnDocument: false,
106
+ accept: {
107
+ "application/images": [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"]
108
+ }
109
+ };
110
+ async function uploadBlogImage(params) {
111
+ if (!params.selection || !params.key) return;
112
+ await S3Service.upload({
113
+ key: params.key,
114
+ contentType: params.selection.contentType,
115
+ file: params.selection.file,
116
+ isPublic: true
117
+ });
118
+ }
119
+ __name(uploadBlogImage, "uploadBlogImage");
120
+ function BlogCoverImageField({
121
+ form,
122
+ id,
123
+ label,
124
+ keyPrefix,
125
+ entityId,
126
+ currentUrl,
127
+ alt,
128
+ selection,
129
+ onSelectionChange
130
+ }) {
131
+ const [files, setFiles] = useState(null);
132
+ useEffect(() => {
133
+ if (selection === null) setFiles(null);
134
+ }, [selection]);
135
+ const previewUrl = useMemo(() => selection ? URL.createObjectURL(selection.file) : void 0, [selection]);
136
+ useEffect(() => {
137
+ return () => {
138
+ if (previewUrl) URL.revokeObjectURL(previewUrl);
139
+ };
140
+ }, [previewUrl]);
141
+ const src = previewUrl ?? currentUrl;
142
+ return /* @__PURE__ */ jsx(FormFieldWrapper, { form, name: id, label, children: (field) => /* @__PURE__ */ jsx(
143
+ FileUploader,
144
+ {
145
+ value: files,
146
+ onValueChange: (value) => {
147
+ setFiles(value);
148
+ const file = value?.[0];
149
+ if (!file) return;
150
+ const extension = file.type.split("/").pop() ?? "";
151
+ field.onChange(`${keyPrefix}/${entityId}/${entityId}.${Date.now()}.${extension}`);
152
+ onSelectionChange({ file, contentType: file.type });
153
+ },
154
+ dropzoneOptions: dropzone,
155
+ className: "w-full",
156
+ children: /* @__PURE__ */ jsx(FileInput, { className: "text-muted-foreground/50 rounded-lg outline-1 outline-dashed", children: /* @__PURE__ */ jsx("div", { className: "flex w-full flex-col items-center justify-center p-4", children: src ? /* @__PURE__ */ jsx(
157
+ Image,
158
+ {
159
+ src,
160
+ alt: alt ?? label,
161
+ width: 320,
162
+ height: 180,
163
+ className: "h-auto max-h-48 w-auto rounded-md object-cover",
164
+ unoptimized: isRemoteImageSrc(previewUrl ? void 0 : currentUrl)
165
+ }
166
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
167
+ /* @__PURE__ */ jsx(UploadIcon, { className: "my-4 h-8 w-8" }),
168
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: label })
169
+ ] }) }) })
170
+ }
171
+ ) });
172
+ }
173
+ __name(BlogCoverImageField, "BlogCoverImageField");
174
+
175
+ // src/features/blog/components/admin/BlogTranslationStatus.tsx
176
+ import { LoaderCircleIcon, RotateCcwIcon } from "lucide-react";
177
+ import { useTranslations } from "next-intl";
178
+ import { useCallback, useMemo as useMemo2, useState as useState2 } from "react";
179
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
180
+ var BLOG_STATUS_BADGE = {
181
+ draft: "softGray",
182
+ published: "softGreen",
183
+ unpublished: "softAmber"
184
+ };
185
+ function blogCategoryAdminName(category) {
186
+ return category.original?.name ?? category.name ?? "";
187
+ }
188
+ __name(blogCategoryAdminName, "blogCategoryAdminName");
189
+ function useBlogLanguageName() {
190
+ const locale = useI18nLocale();
191
+ const names = useMemo2(() => {
192
+ try {
193
+ return new Intl.DisplayNames([locale], { type: "language" });
194
+ } catch {
195
+ return void 0;
196
+ }
197
+ }, [locale]);
198
+ return useCallback(
199
+ (language) => {
200
+ const name = names?.of(language);
201
+ if (!name) return language;
202
+ return name.charAt(0).toLocaleUpperCase(locale) + name.slice(1);
203
+ },
204
+ [names, locale]
205
+ );
206
+ }
207
+ __name(useBlogLanguageName, "useBlogLanguageName");
208
+ function useBlogDateFormatter() {
209
+ const locale = useI18nLocale();
210
+ return useCallback((date) => date ? date.toLocaleDateString(locale, { dateStyle: "medium" }) : "", [locale]);
211
+ }
212
+ __name(useBlogDateFormatter, "useBlogDateFormatter");
213
+ function BlogTranslationStatus({ aiStatus, onRetranslate }) {
214
+ const t = useTranslations();
215
+ const [isRetrying, setIsRetrying] = useState2(false);
216
+ if (aiStatus === "completed") return null;
217
+ if (aiStatus === "pending" || aiStatus === "in_progress") {
218
+ return /* @__PURE__ */ jsx2("div", { className: "flex items-center gap-x-2", children: /* @__PURE__ */ jsxs2(Badge, { variant: "softBlue", children: [
219
+ /* @__PURE__ */ jsx2(LoaderCircleIcon, { className: "animate-spin" }),
220
+ aiStatus === "pending" ? t("blog.translation.pending") : t("blog.translation.translating")
221
+ ] }) });
222
+ }
223
+ const retranslate = /* @__PURE__ */ __name(async () => {
224
+ if (!onRetranslate) return;
225
+ setIsRetrying(true);
226
+ try {
227
+ await onRetranslate();
228
+ } catch (error) {
229
+ errorToast({ title: t("common.errors.error"), error });
230
+ } finally {
231
+ setIsRetrying(false);
232
+ }
233
+ }, "retranslate");
234
+ return /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-x-2", children: [
235
+ /* @__PURE__ */ jsx2(Badge, { variant: "softRed", children: t("blog.translation.failed") }),
236
+ onRetranslate && /* @__PURE__ */ jsxs2(Button, { type: "button", size: "sm", variant: "outline", onClick: () => void retranslate(), disabled: isRetrying, children: [
237
+ /* @__PURE__ */ jsx2(RotateCcwIcon, {}),
238
+ t("blog.actions.translateAgain")
239
+ ] })
240
+ ] });
241
+ }
242
+ __name(BlogTranslationStatus, "BlogTranslationStatus");
243
+
244
+ // src/features/blog/components/admin/BlogCategoryEditor.tsx
245
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
246
+ var buildTranslationSchema = /* @__PURE__ */ __name((t) => z.object({
247
+ id: z.string(),
248
+ name: z.string().min(1, { message: t("blog.errors.name") }),
249
+ slug: z.string().optional(),
250
+ description: z.string().optional()
251
+ }), "buildTranslationSchema");
252
+ var buildFormSchema = /* @__PURE__ */ __name((t) => z.object({
253
+ id: z.string().uuid(),
254
+ originalLanguage: z.string().min(1, { message: t("blog.errors.language") }),
255
+ name: z.string().min(1, { message: t("blog.errors.name") }),
256
+ slug: z.string().optional(),
257
+ description: z.string().optional(),
258
+ image: z.string().optional(),
259
+ translations: z.array(buildTranslationSchema(t))
260
+ }), "buildFormSchema");
261
+ var sameTranslation = /* @__PURE__ */ __name((a, b) => !!b && a.name === b.name && (a.slug ?? "") === (b.slug ?? "") && (a.description ?? "") === (b.description ?? ""), "sameTranslation");
262
+ function BlogCategoryEditor({ category, trigger, onSuccess }) {
263
+ const t = useTranslations2();
264
+ const formSchema = useMemo3(() => buildFormSchema(t), [t]);
265
+ const locale = useI18nLocale();
266
+ const languageName = useBlogLanguageName();
267
+ const [image, setImage] = useState3(null);
268
+ const languages = useMemo3(() => getI18nSupportedLocales(), []);
269
+ const original = category?.original;
270
+ const translations = useMemo3(
271
+ () => (category?.translations ?? []).filter((translation) => !translation.isOriginal),
272
+ [category]
273
+ );
274
+ const getDefaultValues = useCallback2(
275
+ () => ({
276
+ id: category?.id || v4(),
277
+ originalLanguage: category?.originalLanguage ?? (languages.includes(locale) ? locale : languages[0]),
278
+ name: original?.name ?? category?.name ?? "",
279
+ slug: original?.slug ?? "",
280
+ description: original?.description ?? "",
281
+ // The stored value is a signed URL, not a key: only a newly picked file writes a key.
282
+ image: "",
283
+ translations: translations.map((translation) => ({
284
+ id: translation.id,
285
+ name: translation.name ?? "",
286
+ slug: translation.slug ?? "",
287
+ description: translation.description ?? ""
288
+ }))
289
+ }),
290
+ [category, original, translations, languages, locale]
291
+ );
292
+ const form = useForm({
293
+ resolver: zodResolver(formSchema),
294
+ defaultValues: getDefaultValues()
295
+ });
296
+ return /* @__PURE__ */ jsx3(
297
+ EditorSheet,
298
+ {
299
+ form,
300
+ entityType: t("blog.entities.category"),
301
+ entityName: original?.name ?? category?.name,
302
+ title: category ? t("blog.admin.editCategory") : t("blog.admin.newCategory"),
303
+ isEdit: !!category,
304
+ module: Modules.BlogCategory,
305
+ size: "md",
306
+ onSubmit: async (values) => {
307
+ if (values.image && image) await uploadBlogImage({ key: values.image, selection: image });
308
+ const imageKey = values.image && image ? values.image : void 0;
309
+ if (!category) {
310
+ return BlogCategoryService.create({
311
+ id: values.id,
312
+ originalLanguage: values.originalLanguage,
313
+ name: values.name,
314
+ description: values.description,
315
+ image: imageKey
316
+ });
317
+ }
318
+ const defaults = getDefaultValues();
319
+ const originalChanged = !!imageKey || !sameTranslation(
320
+ { id: values.id, name: values.name, slug: values.slug, description: values.description },
321
+ { id: defaults.id, name: defaults.name, slug: defaults.slug, description: defaults.description }
322
+ );
323
+ if (originalChanged) {
324
+ await BlogCategoryService.update({
325
+ id: values.id,
326
+ originalLanguage: category?.originalLanguage,
327
+ name: values.name,
328
+ slug: values.slug || void 0,
329
+ description: values.description,
330
+ image: imageKey
331
+ });
332
+ }
333
+ for (const translation of values.translations) {
334
+ const before = defaults.translations.find((candidate) => candidate.id === translation.id);
335
+ if (sameTranslation(translation, before)) continue;
336
+ await BlogCategoryTranslationService.update({
337
+ id: translation.id,
338
+ name: translation.name,
339
+ slug: translation.slug || void 0,
340
+ description: translation.description
341
+ });
342
+ }
343
+ return { id: values.id };
344
+ },
345
+ onReset: () => {
346
+ setImage(null);
347
+ return getDefaultValues();
348
+ },
349
+ onSuccess: onSuccess ?? (() => {
350
+ }),
351
+ trigger,
352
+ children: /* @__PURE__ */ jsxs3("div", { className: "flex w-full flex-col gap-y-4", children: [
353
+ !category && /* @__PURE__ */ jsx3(
354
+ FormSelect,
355
+ {
356
+ form,
357
+ id: "originalLanguage",
358
+ name: t("blog.fields.originalLanguage"),
359
+ values: languages.map((language) => ({ id: language, text: languageName(language) })),
360
+ isRequired: true
361
+ }
362
+ ),
363
+ /* @__PURE__ */ jsx3(FormInput, { form, id: "name", name: t("blog.fields.name"), isRequired: true }),
364
+ category && /* @__PURE__ */ jsx3(FormInput, { form, id: "slug", name: t("blog.fields.slug"), description: t("blog.fields.slugHelp") }),
365
+ /* @__PURE__ */ jsx3(FormTextarea, { form, id: "description", name: t("blog.fields.description") }),
366
+ /* @__PURE__ */ jsx3(
367
+ BlogCoverImageField,
368
+ {
369
+ form,
370
+ id: "image",
371
+ label: t("blog.fields.image"),
372
+ keyPrefix: BLOG_CATEGORY_UPLOAD_PREFIX,
373
+ entityId: form.getValues("id"),
374
+ currentUrl: category?.image,
375
+ selection: image,
376
+ onSelectionChange: setImage
377
+ }
378
+ ),
379
+ translations.length > 0 && /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-y-2", children: [
380
+ /* @__PURE__ */ jsx3(SectionHeader, { className: "mt-2", children: t("blog.fields.languages") }),
381
+ translations.map((translation, index) => /* @__PURE__ */ jsxs3(Collapsible, { className: "rounded-md border", children: [
382
+ /* @__PURE__ */ jsxs3(CollapsibleTrigger, { className: "flex w-full items-center justify-between gap-x-2 px-3 py-2 text-start", children: [
383
+ /* @__PURE__ */ jsxs3("span", { className: "flex items-center gap-x-2 text-sm font-medium", children: [
384
+ languageName(translation.language),
385
+ /* @__PURE__ */ jsx3(BlogTranslationStatus, { aiStatus: translation.aiStatus })
386
+ ] }),
387
+ /* @__PURE__ */ jsx3(ChevronDownIcon, { className: "text-muted-foreground size-4" })
388
+ ] }),
389
+ /* @__PURE__ */ jsxs3(CollapsibleContent, { className: "flex flex-col gap-y-4 border-t p-3", children: [
390
+ translation.aiStatus === "failed" && /* @__PURE__ */ jsx3(
391
+ BlogTranslationStatus,
392
+ {
393
+ aiStatus: translation.aiStatus,
394
+ onRetranslate: async () => {
395
+ await BlogCategoryTranslationService.retranslate({ id: translation.id });
396
+ await onSuccess?.();
397
+ }
398
+ }
399
+ ),
400
+ /* @__PURE__ */ jsx3(FormInput, { form, id: `translations.${index}.name`, name: t("blog.fields.name"), isRequired: true }),
401
+ /* @__PURE__ */ jsx3(
402
+ FormInput,
403
+ {
404
+ form,
405
+ id: `translations.${index}.slug`,
406
+ name: t("blog.fields.slug"),
407
+ description: t("blog.fields.slugHelp")
408
+ }
409
+ ),
410
+ /* @__PURE__ */ jsx3(
411
+ FormTextarea,
412
+ {
413
+ form,
414
+ id: `translations.${index}.description`,
415
+ name: t("blog.fields.description")
416
+ }
417
+ )
418
+ ] })
419
+ ] }, translation.id))
420
+ ] })
421
+ ] })
422
+ }
423
+ );
424
+ }
425
+ __name(BlogCategoryEditor, "BlogCategoryEditor");
426
+
427
+ // src/features/blog/hooks/useBlogCategoryTableStructure.tsx
428
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
429
+ var useBlogCategoryTableStructure = /* @__PURE__ */ __name((params) => {
430
+ const t = useTranslations3();
431
+ const refresh = params.dataRetriever?.refresh;
432
+ const tableData = useMemo4(() => {
433
+ return params.data.map((category) => {
434
+ const entry = { jsonApiData: category };
435
+ params.fields.forEach((field) => {
436
+ entry[field] = category[field];
437
+ });
438
+ return entry;
439
+ });
440
+ }, [params.data, params.fields]);
441
+ const fieldColumnMap = {
442
+ ["name" /* name */]: () => ({
443
+ id: "name" /* name */,
444
+ accessorKey: "name" /* name */,
445
+ header: t("blog.fields.name"),
446
+ cell: /* @__PURE__ */ __name(({ row }) => {
447
+ const category = row.original.jsonApiData;
448
+ return /* @__PURE__ */ jsx4(
449
+ BlogCategoryEditor,
450
+ {
451
+ category,
452
+ onSuccess: refresh,
453
+ trigger: /* @__PURE__ */ jsx4("button", { type: "button", className: "text-primary text-start font-medium", children: blogCategoryAdminName(category) || category.id })
454
+ }
455
+ );
456
+ }, "cell"),
457
+ enableSorting: false,
458
+ enableHiding: false
459
+ }),
460
+ ["languages" /* languages */]: () => ({
461
+ id: "languages" /* languages */,
462
+ accessorKey: "languages" /* languages */,
463
+ header: t("blog.fields.languages"),
464
+ cell: /* @__PURE__ */ __name(({ row }) => {
465
+ const category = row.original.jsonApiData;
466
+ const total = category.translations.length;
467
+ if (total === 0) return /* @__PURE__ */ jsx4("span", { className: "text-muted-foreground text-xs", children: "\u2014" });
468
+ const ready = category.translations.filter((translation) => translation.aiStatus === "completed").length;
469
+ return /* @__PURE__ */ jsxs4("span", { className: "block text-xs tabular-nums text-end", children: [
470
+ ready,
471
+ "/",
472
+ total
473
+ ] });
474
+ }, "cell"),
475
+ enableSorting: false,
476
+ enableHiding: false
477
+ })
478
+ };
479
+ const columns = useMemo4(() => {
480
+ return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== void 0);
481
+ }, [params.fields, fieldColumnMap, t, refresh]);
482
+ return useMemo4(() => ({ data: tableData, columns }), [tableData, columns]);
483
+ }, "useBlogCategoryTableStructure");
484
+ registerTableGenerator("blog-categories", useBlogCategoryTableStructure);
485
+
486
+ // src/features/blog/components/admin/BlogCategoriesList.tsx
487
+ import { jsx as jsx5 } from "react/jsx-runtime";
488
+ function BlogCategoriesList({ fullWidth } = {}) {
489
+ const t = useTranslations4();
490
+ const data = useDataListRetriever({
491
+ retriever: /* @__PURE__ */ __name((params) => BlogCategoryService.findMany(params), "retriever"),
492
+ retrieverParams: {},
493
+ module: Modules.BlogCategory
494
+ });
495
+ const functions = [
496
+ /* @__PURE__ */ jsx5(
497
+ BlogCategoryEditor,
498
+ {
499
+ onSuccess: data.refresh,
500
+ trigger: /* @__PURE__ */ jsx5(Button, { render: /* @__PURE__ */ jsx5("div", {}), nativeButton: false, size: "sm", variant: "outline", children: t("blog.admin.newCategory") })
501
+ },
502
+ "create-blog-category"
503
+ )
504
+ ];
505
+ return /* @__PURE__ */ jsx5(
506
+ ContentListTable,
507
+ {
508
+ data,
509
+ fields: ["name" /* name */, "languages" /* languages */],
510
+ tableGeneratorType: Modules.BlogCategory,
511
+ functions,
512
+ fullWidth,
513
+ title: t("blog.entities.categories"),
514
+ emptyState: /* @__PURE__ */ jsx5(EmptyState, { icon: TagIcon, title: t("blog.admin.emptyCategories") })
515
+ }
516
+ );
517
+ }
518
+ __name(BlogCategoriesList, "BlogCategoriesList");
519
+
520
+ // src/features/blog/components/admin/BlogLayoutSetting.tsx
521
+ import { useTranslations as useTranslations5 } from "next-intl";
522
+ import { useEffect as useEffect2, useState as useState4 } from "react";
523
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
524
+ function BlogLayoutSetting() {
525
+ const t = useTranslations5();
526
+ const [layout, setLayout] = useState4(void 0);
527
+ const [saving, setSaving] = useState4(false);
528
+ useEffect2(() => {
529
+ let active = true;
530
+ BlogSettingsService.get().then((settings) => {
531
+ if (active) setLayout(settings?.listLayout ?? "featured");
532
+ }).catch(() => {
533
+ if (active) setLayout("featured");
534
+ });
535
+ return () => {
536
+ active = false;
537
+ };
538
+ }, []);
539
+ const options = {
540
+ featured: { label: t("blog.layout.featured"), description: t("blog.layout.featuredDescription") },
541
+ column: { label: t("blog.layout.column"), description: t("blog.layout.columnDescription") },
542
+ rail: { label: t("blog.layout.rail"), description: t("blog.layout.railDescription") }
543
+ };
544
+ const save = /* @__PURE__ */ __name(async (next) => {
545
+ const previous = layout;
546
+ setLayout(next);
547
+ setSaving(true);
548
+ try {
549
+ await BlogSettingsService.save({ id: BLOG_SETTINGS_ID, listLayout: next });
550
+ showToast(t("blog.layout.saved"));
551
+ } catch (error) {
552
+ setLayout(previous);
553
+ errorToast({ title: t("common.errors.error"), error });
554
+ } finally {
555
+ setSaving(false);
556
+ }
557
+ }, "save");
558
+ return /* @__PURE__ */ jsxs5("div", { className: "flex w-full max-w-3xl flex-col gap-y-4 p-4", children: [
559
+ /* @__PURE__ */ jsx6(SectionHeader, { children: t("blog.layout.title") }),
560
+ layout && /* @__PURE__ */ jsx6(
561
+ RadioGroup,
562
+ {
563
+ value: layout,
564
+ onValueChange: (value) => {
565
+ if (!value || value === layout) return;
566
+ void save(value);
567
+ },
568
+ disabled: saving,
569
+ className: "grid grid-cols-1 gap-3 md:grid-cols-3",
570
+ children: BLOG_LIST_LAYOUTS.map((option) => /* @__PURE__ */ jsxs5(
571
+ "label",
572
+ {
573
+ htmlFor: `blog-layout-${option}`,
574
+ className: "has-data-checked:border-primary flex cursor-pointer items-start gap-x-3 rounded-lg border p-4",
575
+ children: [
576
+ /* @__PURE__ */ jsx6(RadioGroupItem, { value: option, id: `blog-layout-${option}`, className: "mt-0.5" }),
577
+ /* @__PURE__ */ jsxs5("span", { className: "flex flex-col gap-y-1", children: [
578
+ /* @__PURE__ */ jsx6("span", { className: "text-sm font-medium", children: options[option].label }),
579
+ /* @__PURE__ */ jsx6("span", { className: "text-muted-foreground text-xs/relaxed", children: options[option].description })
580
+ ] })
581
+ ]
582
+ },
583
+ option
584
+ ))
585
+ }
586
+ )
587
+ ] });
588
+ }
589
+ __name(BlogLayoutSetting, "BlogLayoutSetting");
590
+
591
+ // src/features/blog/components/admin/BlogPostsList.tsx
592
+ import { NewspaperIcon } from "lucide-react";
593
+ import { useTranslations as useTranslations8 } from "next-intl";
594
+
595
+ // src/features/blog/hooks/useBlogPostTableStructure.tsx
596
+ import { useTranslations as useTranslations6 } from "next-intl";
597
+ import { useMemo as useMemo5 } from "react";
598
+
599
+ // src/features/blog/hooks/useBlogCategoryNames.ts
600
+ import { useCallback as useCallback3, useEffect as useEffect3, useState as useState5 } from "react";
601
+ function useBlogCategoryNames() {
602
+ const [names, setNames] = useState5({});
603
+ useEffect3(() => {
604
+ let cancelled = false;
605
+ BlogCategoryService.findMany({ fetchAll: true }).then((categories) => {
606
+ if (cancelled) return;
607
+ setNames(Object.fromEntries(categories.map((category) => [category.id, blogCategoryAdminName(category)])));
608
+ }).catch(() => {
609
+ });
610
+ return () => {
611
+ cancelled = true;
612
+ };
613
+ }, []);
614
+ return useCallback3((categoryId) => names[categoryId] ?? "", [names]);
615
+ }
616
+ __name(useBlogCategoryNames, "useBlogCategoryNames");
617
+
618
+ // src/features/blog/hooks/useBlogPostTableStructure.tsx
619
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
620
+ var useBlogPostTableStructure = /* @__PURE__ */ __name((params) => {
621
+ const t = useTranslations6();
622
+ const locale = useI18nLocale();
623
+ const generateUrl = usePageUrlGenerator();
624
+ const categoryName = useBlogCategoryNames();
625
+ const tableData = useMemo5(() => {
626
+ return params.data.map((post) => {
627
+ const entry = { jsonApiData: post };
628
+ params.fields.forEach((field) => {
629
+ entry[field] = post[field];
630
+ });
631
+ return entry;
632
+ });
633
+ }, [params.data, params.fields]);
634
+ const fieldColumnMap = {
635
+ ["title" /* title */]: () => ({
636
+ id: "title" /* title */,
637
+ accessorKey: "title" /* title */,
638
+ header: t("blog.fields.title"),
639
+ cell: /* @__PURE__ */ __name(({ row }) => {
640
+ const post = row.original.jsonApiData;
641
+ return /* @__PURE__ */ jsx7(Link, { href: generateUrl({ page: Modules.BlogPost, id: post.id }), children: post.original?.title ?? post.title ?? post.id });
642
+ }, "cell"),
643
+ enableSorting: false,
644
+ enableHiding: false
645
+ }),
646
+ ["status" /* status */]: () => ({
647
+ id: "status" /* status */,
648
+ accessorKey: "status" /* status */,
649
+ header: t("blog.fields.status"),
650
+ cell: /* @__PURE__ */ __name(({ row }) => {
651
+ const post = row.original.jsonApiData;
652
+ const label = {
653
+ draft: t("blog.status.draft"),
654
+ published: t("blog.status.published"),
655
+ unpublished: t("blog.status.unpublished")
656
+ }[post.status];
657
+ return /* @__PURE__ */ jsx7(Badge, { variant: BLOG_STATUS_BADGE[post.status] ?? "softGray", children: label ?? post.status });
658
+ }, "cell"),
659
+ enableSorting: false,
660
+ enableHiding: false
661
+ }),
662
+ ["categories" /* categories */]: () => ({
663
+ id: "categories" /* categories */,
664
+ accessorKey: "categories" /* categories */,
665
+ header: t("blog.fields.categories"),
666
+ cell: /* @__PURE__ */ __name(({ row }) => {
667
+ const post = row.original.jsonApiData;
668
+ const names = post.categories.map((category) => categoryName(category.id)).filter(Boolean);
669
+ if (names.length === 0) return /* @__PURE__ */ jsx7("span", { className: "text-muted-foreground text-xs", children: "\u2014" });
670
+ return /* @__PURE__ */ jsx7("span", { className: "text-xs", children: names.join(", ") });
671
+ }, "cell"),
672
+ enableSorting: false,
673
+ enableHiding: false
674
+ }),
675
+ ["publishedAt" /* publishedAt */]: () => ({
676
+ id: "publishedAt" /* publishedAt */,
677
+ accessorKey: "publishedAt" /* publishedAt */,
678
+ header: t("blog.fields.publishedAt"),
679
+ cell: /* @__PURE__ */ __name(({ row }) => {
680
+ const post = row.original.jsonApiData;
681
+ if (!post.publishedAt) return /* @__PURE__ */ jsx7("span", { className: "text-muted-foreground text-xs", children: "\u2014" });
682
+ return /* @__PURE__ */ jsx7("span", { className: "text-xs tabular-nums", children: post.publishedAt.toLocaleDateString(locale, { dateStyle: "medium" }) });
683
+ }, "cell"),
684
+ enableSorting: false,
685
+ enableHiding: false
686
+ }),
687
+ ["languages" /* languages */]: () => ({
688
+ id: "languages" /* languages */,
689
+ accessorKey: "languages" /* languages */,
690
+ header: t("blog.fields.languages"),
691
+ cell: /* @__PURE__ */ __name(({ row }) => {
692
+ const post = row.original.jsonApiData;
693
+ const total = post.translations.length;
694
+ if (total === 0) return /* @__PURE__ */ jsx7("span", { className: "text-muted-foreground text-xs", children: "\u2014" });
695
+ const ready = post.translations.filter((translation) => translation.aiStatus === "completed").length;
696
+ return /* @__PURE__ */ jsxs6("span", { className: "block text-xs tabular-nums text-end", children: [
697
+ ready,
698
+ "/",
699
+ total
700
+ ] });
701
+ }, "cell"),
702
+ enableSorting: false,
703
+ enableHiding: false
704
+ })
705
+ };
706
+ const columns = useMemo5(() => {
707
+ return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== void 0);
708
+ }, [params.fields, fieldColumnMap, t, generateUrl, locale, categoryName]);
709
+ return useMemo5(() => ({ data: tableData, columns }), [tableData, columns]);
710
+ }, "useBlogPostTableStructure");
711
+ registerTableGenerator("blog-posts", useBlogPostTableStructure);
712
+
713
+ // src/features/blog/components/admin/BlogPostEditor.tsx
714
+ import { zodResolver as zodResolver2 } from "@hookform/resolvers/zod";
715
+ import { useTranslations as useTranslations7 } from "next-intl";
716
+ import { useCallback as useCallback4, useMemo as useMemo6, useState as useState6 } from "react";
717
+ import { useForm as useForm2 } from "react-hook-form";
718
+ import { v4 as v42 } from "uuid";
719
+ import { z as z2 } from "zod";
720
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
721
+ var isBodyEmpty = /* @__PURE__ */ __name((body) => !body || Array.isArray(body) && body.length === 0, "isBodyEmpty");
722
+ function BlogPostEditor({ post, trigger, onSuccess }) {
723
+ const t = useTranslations7();
724
+ const categoryName = useBlogCategoryNames();
725
+ const router = useI18nRouter();
726
+ const locale = useI18nLocale();
727
+ const languageName = useBlogLanguageName();
728
+ const original = post?.original;
729
+ const [cover, setCover] = useState6(null);
730
+ const [isBodyEmptyState, setIsBodyEmptyState] = useState6(isBodyEmpty(original?.body));
731
+ const languages = useMemo6(() => getI18nSupportedLocales(), []);
732
+ const formSchema = useMemo6(
733
+ () => z2.object({
734
+ id: z2.string().uuid(),
735
+ originalLanguage: z2.string().min(1, { message: t("blog.errors.language") }),
736
+ title: z2.string().min(1, { message: t("blog.errors.title") }),
737
+ slug: z2.string().optional(),
738
+ summary: z2.string().optional(),
739
+ body: z2.any(),
740
+ coverImage: z2.string().optional(),
741
+ coverImageAlt: z2.string().optional(),
742
+ categories: z2.array(z2.object({ id: z2.string(), name: z2.string() }))
743
+ }),
744
+ []
745
+ );
746
+ const getDefaultValues = useCallback4(
747
+ () => ({
748
+ id: post?.id || v42(),
749
+ originalLanguage: post?.originalLanguage ?? (languages.includes(locale) ? locale : languages[0]),
750
+ title: original?.title ?? "",
751
+ slug: original?.slug ?? "",
752
+ summary: original?.summary ?? "",
753
+ body: original?.body || [],
754
+ // The stored value is a signed URL, not a key: only a newly picked file
755
+ // writes a key here, and only then is the field sent.
756
+ coverImage: "",
757
+ coverImageAlt: original?.coverImageAlt ?? "",
758
+ categories: (post?.categories ?? []).map((category) => ({
759
+ id: category.id,
760
+ name: blogCategoryAdminName(category)
761
+ }))
762
+ }),
763
+ [post, original, languages, locale]
764
+ );
765
+ const form = useForm2({
766
+ resolver: zodResolver2(formSchema),
767
+ defaultValues: getDefaultValues()
768
+ });
769
+ const { dirtyFields } = form.formState;
770
+ const isFormDirty = useCallback4(() => {
771
+ const dirty = { ...dirtyFields };
772
+ if (dirty.body && isBodyEmptyState) delete dirty.body;
773
+ return Object.keys(dirty).length > 0;
774
+ }, [dirtyFields, isBodyEmptyState]);
775
+ return /* @__PURE__ */ jsx8(
776
+ EditorSheet,
777
+ {
778
+ form,
779
+ entityType: t("blog.entities.post"),
780
+ entityName: original?.title,
781
+ title: post ? t("blog.admin.editPost") : t("blog.admin.newPost"),
782
+ isEdit: !!post,
783
+ module: Modules.BlogPost,
784
+ size: "lg",
785
+ isFormDirty,
786
+ onSubmit: async (values) => {
787
+ if (values.coverImage && cover) await uploadBlogImage({ key: values.coverImage, selection: cover });
788
+ const payload = {
789
+ id: values.id,
790
+ title: values.title,
791
+ // An empty summary lets the worker write one; on edit the summary is
792
+ // sent only when changed here, so an untouched AI summary is regenerated.
793
+ summary: post ? form.getFieldState("summary").isDirty ? values.summary : void 0 : values.summary || void 0,
794
+ body: values.body,
795
+ coverImage: values.coverImage && cover ? values.coverImage : void 0,
796
+ coverImageAlt: values.coverImageAlt,
797
+ categoryIds: values.categories.map((category) => category.id)
798
+ };
799
+ if (post) {
800
+ return BlogPostService.update({
801
+ ...payload,
802
+ originalLanguage: post.originalLanguage,
803
+ slug: values.slug || void 0
804
+ });
805
+ }
806
+ return BlogPostService.create({ ...payload, originalLanguage: values.originalLanguage });
807
+ },
808
+ onReset: () => {
809
+ setCover(null);
810
+ setIsBodyEmptyState(isBodyEmpty(original?.body));
811
+ return getDefaultValues();
812
+ },
813
+ onSuccess,
814
+ onNavigate: (url) => router.push(url),
815
+ trigger,
816
+ children: /* @__PURE__ */ jsxs7("div", { className: "flex w-full flex-col gap-y-4", children: [
817
+ !post && /* @__PURE__ */ jsx8(
818
+ FormSelect,
819
+ {
820
+ form,
821
+ id: "originalLanguage",
822
+ name: t("blog.fields.originalLanguage"),
823
+ values: languages.map((language) => ({ id: language, text: languageName(language) })),
824
+ isRequired: true
825
+ }
826
+ ),
827
+ /* @__PURE__ */ jsx8(FormInput, { form, id: "title", name: t("blog.fields.title"), isRequired: true }),
828
+ post && /* @__PURE__ */ jsx8(FormInput, { form, id: "slug", name: t("blog.fields.slug"), description: t("blog.fields.slugHelp") }),
829
+ /* @__PURE__ */ jsx8(
830
+ FormTextarea,
831
+ {
832
+ form,
833
+ id: "summary",
834
+ name: t("blog.fields.summary"),
835
+ description: t("blog.fields.summaryHelp")
836
+ }
837
+ ),
838
+ /* @__PURE__ */ jsx8(
839
+ BlogCoverImageField,
840
+ {
841
+ form,
842
+ id: "coverImage",
843
+ label: t("blog.fields.coverImage"),
844
+ keyPrefix: BLOG_POST_UPLOAD_PREFIX,
845
+ entityId: form.getValues("id"),
846
+ currentUrl: post?.coverImage,
847
+ alt: original?.coverImageAlt,
848
+ selection: cover,
849
+ onSelectionChange: setCover
850
+ }
851
+ ),
852
+ /* @__PURE__ */ jsx8(FormInput, { form, id: "coverImageAlt", name: t("blog.fields.coverImageAlt") }),
853
+ /* @__PURE__ */ jsx8(
854
+ EntityMultiSelector,
855
+ {
856
+ id: "categories",
857
+ form,
858
+ label: t("blog.fields.categories"),
859
+ retriever: (params) => BlogCategoryService.findMany(params),
860
+ module: Modules.BlogCategory,
861
+ getLabel: blogCategoryAdminName,
862
+ getFormValueLabel: (value) => value.name || categoryName(value.id)
863
+ }
864
+ ),
865
+ /* @__PURE__ */ jsx8(
866
+ FormBlockNote,
867
+ {
868
+ uploadKeyPrefix: BLOG_POST_UPLOAD_PREFIX,
869
+ form,
870
+ id: "body",
871
+ name: t("blog.fields.body"),
872
+ type: "blog-post",
873
+ isRequired: true,
874
+ onEmptyChange: setIsBodyEmptyState
875
+ }
876
+ )
877
+ ] })
878
+ }
879
+ );
880
+ }
881
+ __name(BlogPostEditor, "BlogPostEditor");
882
+
883
+ // src/features/blog/components/admin/BlogPostsList.tsx
884
+ import { jsx as jsx9 } from "react/jsx-runtime";
885
+ function BlogPostsList({ fullWidth } = {}) {
886
+ const t = useTranslations8();
887
+ const data = useDataListRetriever({
888
+ retriever: /* @__PURE__ */ __name((params) => BlogPostService.findMany(params), "retriever"),
889
+ retrieverParams: {},
890
+ module: Modules.BlogPost
891
+ });
892
+ const functions = [
893
+ /* @__PURE__ */ jsx9(
894
+ BlogPostEditor,
895
+ {
896
+ trigger: /* @__PURE__ */ jsx9(Button, { render: /* @__PURE__ */ jsx9("div", {}), nativeButton: false, size: "sm", variant: "outline", children: t("blog.admin.newPost") })
897
+ },
898
+ "create-blog-post"
899
+ )
900
+ ];
901
+ return /* @__PURE__ */ jsx9(
902
+ ContentListTable,
903
+ {
904
+ data,
905
+ fields: [
906
+ "title" /* title */,
907
+ "status" /* status */,
908
+ "categories" /* categories */,
909
+ "publishedAt" /* publishedAt */,
910
+ "languages" /* languages */
911
+ ],
912
+ tableGeneratorType: Modules.BlogPost,
913
+ functions,
914
+ fullWidth,
915
+ title: t("blog.entities.posts"),
916
+ emptyState: /* @__PURE__ */ jsx9(EmptyState, { icon: NewspaperIcon, title: t("blog.admin.empty") })
917
+ }
918
+ );
919
+ }
920
+ __name(BlogPostsList, "BlogPostsList");
921
+
922
+ // src/features/blog/components/admin/BlogAdminContainer.tsx
923
+ import { jsx as jsx10 } from "react/jsx-runtime";
924
+ function BlogAdminContainer() {
925
+ const t = useTranslations9();
926
+ return /* @__PURE__ */ jsx10(AdministrationProvider, { children: /* @__PURE__ */ jsx10(
927
+ RoundPageContainer,
928
+ {
929
+ module: Modules.BlogPost,
930
+ fullWidth: true,
931
+ tabs: [
932
+ { sectionKey: "posts", label: t("blog.tabs.posts"), content: /* @__PURE__ */ jsx10(BlogPostsList, { fullWidth: true }) },
933
+ { sectionKey: "categories", label: t("blog.tabs.categories"), content: /* @__PURE__ */ jsx10(BlogCategoriesList, { fullWidth: true }) },
934
+ { sectionKey: "layout", label: t("blog.tabs.layout"), content: /* @__PURE__ */ jsx10(BlogLayoutSetting, {}) }
935
+ ]
936
+ }
937
+ ) });
938
+ }
939
+ __name(BlogAdminContainer, "BlogAdminContainer");
940
+
941
+ // src/features/blog/components/admin/BlogPostContainer.tsx
942
+ import { useTranslations as useTranslations15 } from "next-intl";
943
+ import { useMemo as useMemo9 } from "react";
944
+
945
+ // src/features/blog/contexts/BlogPostContext.tsx
946
+ import { createContext, useCallback as useCallback5, useContext, useEffect as useEffect4, useMemo as useMemo7, useState as useState7 } from "react";
947
+ import { jsx as jsx11 } from "react/jsx-runtime";
948
+ var TRANSLATION_POLL_MS = 5e3;
949
+ var BlogPostContext = createContext(void 0);
950
+ var BlogPostProvider = /* @__PURE__ */ __name(({ postId, children }) => {
951
+ const [post, setPost] = useState7(void 0);
952
+ const reload = useCallback5(async () => {
953
+ const loaded = await BlogPostService.findOne({ id: postId });
954
+ setPost(loaded);
955
+ }, [postId]);
956
+ useEffect4(() => {
957
+ void reload();
958
+ }, [reload]);
959
+ const isTranslating = useMemo7(
960
+ () => (post?.translations ?? []).some(
961
+ (translation) => translation.aiStatus === "pending" || translation.aiStatus === "in_progress"
962
+ ),
963
+ [post]
964
+ );
965
+ useEffect4(() => {
966
+ if (!isTranslating) return;
967
+ const handle = setInterval(() => {
968
+ reload().catch(() => {
969
+ });
970
+ }, TRANSLATION_POLL_MS);
971
+ return () => clearInterval(handle);
972
+ }, [isTranslating, reload]);
973
+ const value = useMemo7(() => ({ post, reload }), [post, reload]);
974
+ return /* @__PURE__ */ jsx11(BlogPostContext.Provider, { value, children });
975
+ }, "BlogPostProvider");
976
+ var useBlogPost = /* @__PURE__ */ __name(() => {
977
+ const context = useContext(BlogPostContext);
978
+ if (context === void 0) {
979
+ throw new Error("useBlogPost must be used within a BlogPostProvider");
980
+ }
981
+ return context;
982
+ }, "useBlogPost");
983
+
984
+ // src/features/blog/components/admin/BlogPostDeleter.tsx
985
+ import { useTranslations as useTranslations10 } from "next-intl";
986
+ import { jsx as jsx12 } from "react/jsx-runtime";
987
+ function BlogPostDeleter() {
988
+ const t = useTranslations10();
989
+ const generateUrl = usePageUrlGenerator();
990
+ const { post } = useBlogPost();
991
+ if (!post) return null;
992
+ const type = t("blog.entities.post");
993
+ return /* @__PURE__ */ jsx12(
994
+ CommonDeleter,
995
+ {
996
+ title: t("common.delete.title", { type }),
997
+ subtitle: t("common.delete.subtitle", { type }),
998
+ description: /* @__PURE__ */ jsx12("div", { className: "text-destructive p-4 text-sm", children: t("common.delete.description", { type }) }),
999
+ showLabel: true,
1000
+ deleteFunction: () => BlogPostService.delete({ id: post.id }),
1001
+ redirectTo: generateUrl({ page: Modules.BlogPost })
1002
+ }
1003
+ );
1004
+ }
1005
+ __name(BlogPostDeleter, "BlogPostDeleter");
1006
+
1007
+ // src/features/blog/components/admin/BlogPostPublishButton.tsx
1008
+ import { EyeIcon, EyeOffIcon } from "lucide-react";
1009
+ import { useTranslations as useTranslations11 } from "next-intl";
1010
+ import { useState as useState8 } from "react";
1011
+ import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
1012
+ function BlogPostPublishButton() {
1013
+ const t = useTranslations11();
1014
+ const { post, reload } = useBlogPost();
1015
+ const [saving, setSaving] = useState8(false);
1016
+ if (!post) return null;
1017
+ const isPublished = post.status === "published";
1018
+ const toggle = /* @__PURE__ */ __name(async () => {
1019
+ setSaving(true);
1020
+ try {
1021
+ if (isPublished) await BlogPostService.unpublish({ id: post.id });
1022
+ else await BlogPostService.publish({ id: post.id });
1023
+ await reload();
1024
+ } catch (error) {
1025
+ errorToast({ title: t("common.errors.error"), error });
1026
+ } finally {
1027
+ setSaving(false);
1028
+ }
1029
+ }, "toggle");
1030
+ return /* @__PURE__ */ jsxs8(
1031
+ Button,
1032
+ {
1033
+ type: "button",
1034
+ size: "sm",
1035
+ variant: isPublished ? "outline" : "default",
1036
+ onClick: () => void toggle(),
1037
+ disabled: saving,
1038
+ children: [
1039
+ isPublished ? /* @__PURE__ */ jsx13(EyeOffIcon, {}) : /* @__PURE__ */ jsx13(EyeIcon, {}),
1040
+ isPublished ? t("blog.actions.unpublish") : t("blog.actions.publish")
1041
+ ]
1042
+ }
1043
+ );
1044
+ }
1045
+ __name(BlogPostPublishButton, "BlogPostPublishButton");
1046
+
1047
+ // src/features/blog/components/admin/BlogPostTranslationPanel.tsx
1048
+ import { PencilIcon } from "lucide-react";
1049
+ import { useTranslations as useTranslations14 } from "next-intl";
1050
+ import Image2 from "next/image";
1051
+ import { useState as useState10 } from "react";
1052
+
1053
+ // src/features/blog/components/admin/BlogPostComparison.tsx
1054
+ import { useTranslations as useTranslations12 } from "next-intl";
1055
+ import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
1056
+ function BlogPostTranslationFields({ translation }) {
1057
+ const t = useTranslations12();
1058
+ return /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-y-4", children: [
1059
+ /* @__PURE__ */ jsx14(DetailField, { label: t("blog.fields.title"), value: translation.title }),
1060
+ /* @__PURE__ */ jsx14(DetailField, { label: t("blog.fields.slug"), value: translation.slug }),
1061
+ /* @__PURE__ */ jsx14(DetailField, { label: t("blog.fields.summary"), value: translation.summary }),
1062
+ /* @__PURE__ */ jsx14(DetailField, { label: t("blog.fields.coverImageAlt"), value: translation.coverImageAlt }),
1063
+ /* @__PURE__ */ jsx14(DetailField, { label: t("blog.fields.body"), children: /* @__PURE__ */ jsx14(BlockNoteEditorContainer, { id: translation.id, type: "blog-post", initialContent: translation.body }) })
1064
+ ] });
1065
+ }
1066
+ __name(BlogPostTranslationFields, "BlogPostTranslationFields");
1067
+ function BlogPostComparison({ original, translation }) {
1068
+ const t = useTranslations12();
1069
+ const languageName = useBlogLanguageName();
1070
+ return /* @__PURE__ */ jsxs9("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2", children: [
1071
+ /* @__PURE__ */ jsxs9("div", { className: "bg-muted/40 flex flex-col gap-y-4 rounded-lg border p-4", children: [
1072
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-x-2", children: [
1073
+ /* @__PURE__ */ jsx14(MicroLabel, { as: "h3", children: languageName(original.language) }),
1074
+ /* @__PURE__ */ jsx14(Badge, { variant: "softGray", children: t("blog.translation.original") })
1075
+ ] }),
1076
+ /* @__PURE__ */ jsx14(BlogPostTranslationFields, { translation: original })
1077
+ ] }),
1078
+ /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-y-4 rounded-lg border p-4", children: [
1079
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-x-2", children: [
1080
+ /* @__PURE__ */ jsx14(MicroLabel, { as: "h3", children: languageName(translation.language) }),
1081
+ translation.source === "manual" && /* @__PURE__ */ jsx14(Badge, { variant: "softAmber", children: t("blog.translation.editedByHand") })
1082
+ ] }),
1083
+ /* @__PURE__ */ jsx14(BlogPostTranslationFields, { translation })
1084
+ ] })
1085
+ ] });
1086
+ }
1087
+ __name(BlogPostComparison, "BlogPostComparison");
1088
+
1089
+ // src/features/blog/components/admin/BlogPostTranslationEditor.tsx
1090
+ import { zodResolver as zodResolver3 } from "@hookform/resolvers/zod";
1091
+ import { useTranslations as useTranslations13 } from "next-intl";
1092
+ import { useCallback as useCallback6, useMemo as useMemo8, useState as useState9 } from "react";
1093
+ import { useForm as useForm3 } from "react-hook-form";
1094
+ import { z as z3 } from "zod";
1095
+ import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
1096
+ var isBodyEmpty2 = /* @__PURE__ */ __name((body) => !body || Array.isArray(body) && body.length === 0, "isBodyEmpty");
1097
+ var buildFormSchema2 = /* @__PURE__ */ __name((t) => z3.object({
1098
+ id: z3.string().uuid(),
1099
+ title: z3.string().min(1, { message: t("blog.errors.title") }),
1100
+ slug: z3.string().optional(),
1101
+ summary: z3.string().optional(),
1102
+ body: z3.any(),
1103
+ coverImageAlt: z3.string().optional()
1104
+ }), "buildFormSchema");
1105
+ function BlogPostTranslationEditor({ translation, trigger, onSuccess }) {
1106
+ const t = useTranslations13();
1107
+ const formSchema = useMemo8(() => buildFormSchema2(t), [t]);
1108
+ const languageName = useBlogLanguageName();
1109
+ const [isBodyEmptyState, setIsBodyEmptyState] = useState9(isBodyEmpty2(translation.body));
1110
+ const getDefaultValues = useCallback6(
1111
+ () => ({
1112
+ id: translation.id,
1113
+ title: translation.title ?? "",
1114
+ slug: translation.slug ?? "",
1115
+ summary: translation.summary ?? "",
1116
+ body: translation.body || [],
1117
+ coverImageAlt: translation.coverImageAlt ?? ""
1118
+ }),
1119
+ [translation]
1120
+ );
1121
+ const form = useForm3({
1122
+ resolver: zodResolver3(formSchema),
1123
+ defaultValues: getDefaultValues()
1124
+ });
1125
+ const { dirtyFields } = form.formState;
1126
+ const isFormDirty = useCallback6(() => {
1127
+ const dirty = { ...dirtyFields };
1128
+ if (dirty.body && isBodyEmptyState) delete dirty.body;
1129
+ return Object.keys(dirty).length > 0;
1130
+ }, [dirtyFields, isBodyEmptyState]);
1131
+ return /* @__PURE__ */ jsx15(
1132
+ EditorSheet,
1133
+ {
1134
+ form,
1135
+ entityType: t("blog.entities.post"),
1136
+ entityName: translation.title,
1137
+ title: `${t("blog.admin.editPost")} \xB7 ${languageName(translation.language)}`,
1138
+ isEdit: true,
1139
+ module: Modules.BlogPostTranslation,
1140
+ size: "lg",
1141
+ isFormDirty,
1142
+ onSubmit: async (values) => BlogPostTranslationService.update({
1143
+ id: values.id,
1144
+ title: values.title,
1145
+ slug: values.slug || void 0,
1146
+ summary: values.summary,
1147
+ body: values.body,
1148
+ coverImageAlt: values.coverImageAlt
1149
+ }),
1150
+ onReset: () => {
1151
+ setIsBodyEmptyState(isBodyEmpty2(translation.body));
1152
+ return getDefaultValues();
1153
+ },
1154
+ onSuccess: onSuccess ?? (() => {
1155
+ }),
1156
+ trigger,
1157
+ children: /* @__PURE__ */ jsxs10("div", { className: "flex w-full flex-col gap-y-4", children: [
1158
+ /* @__PURE__ */ jsx15(FormInput, { form, id: "title", name: t("blog.fields.title"), isRequired: true }),
1159
+ /* @__PURE__ */ jsx15(FormInput, { form, id: "slug", name: t("blog.fields.slug"), description: t("blog.fields.slugHelp") }),
1160
+ /* @__PURE__ */ jsx15(FormTextarea, { form, id: "summary", name: t("blog.fields.summary") }),
1161
+ /* @__PURE__ */ jsx15(FormInput, { form, id: "coverImageAlt", name: t("blog.fields.coverImageAlt") }),
1162
+ /* @__PURE__ */ jsx15(
1163
+ FormBlockNote,
1164
+ {
1165
+ uploadKeyPrefix: BLOG_POST_UPLOAD_PREFIX,
1166
+ form,
1167
+ id: "body",
1168
+ name: t("blog.fields.body"),
1169
+ type: "blog-post",
1170
+ isRequired: true,
1171
+ onEmptyChange: setIsBodyEmptyState
1172
+ }
1173
+ )
1174
+ ] })
1175
+ }
1176
+ );
1177
+ }
1178
+ __name(BlogPostTranslationEditor, "BlogPostTranslationEditor");
1179
+
1180
+ // src/features/blog/components/admin/BlogPostTranslationPanel.tsx
1181
+ import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
1182
+ function BlogPostTranslationPanel({ post, translation, onChanged }) {
1183
+ const t = useTranslations14();
1184
+ const categoryName = useBlogCategoryNames();
1185
+ const languageName = useBlogLanguageName();
1186
+ const formatDate = useBlogDateFormatter();
1187
+ const [isComparing, setIsComparing] = useState10(false);
1188
+ const original = post.original;
1189
+ const reload = onChanged ?? (async () => {
1190
+ });
1191
+ const editTrigger = /* @__PURE__ */ jsxs11(Button, { render: /* @__PURE__ */ jsx16("div", {}), nativeButton: false, size: "sm", variant: "outline", children: [
1192
+ /* @__PURE__ */ jsx16(PencilIcon, {}),
1193
+ t("blog.actions.edit")
1194
+ ] });
1195
+ if (translation.isOriginal) {
1196
+ const statusLabel = {
1197
+ draft: t("blog.status.draft"),
1198
+ published: t("blog.status.published"),
1199
+ unpublished: t("blog.status.unpublished")
1200
+ }[post.status];
1201
+ return /* @__PURE__ */ jsxs11("div", { className: "flex w-full flex-col gap-y-6 p-4", children: [
1202
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between gap-x-4", children: [
1203
+ /* @__PURE__ */ jsx16(Badge, { variant: "softGray", children: t("blog.translation.original") }),
1204
+ /* @__PURE__ */ jsx16(BlogPostEditor, { post, trigger: editTrigger, onSuccess: reload })
1205
+ ] }),
1206
+ post.coverImage && /* @__PURE__ */ jsx16(
1207
+ Image2,
1208
+ {
1209
+ src: post.coverImage,
1210
+ alt: translation.coverImageAlt ?? translation.title,
1211
+ width: 640,
1212
+ height: 360,
1213
+ className: "h-auto max-h-72 w-auto rounded-lg object-cover",
1214
+ unoptimized: isRemoteImageSrc(post.coverImage)
1215
+ }
1216
+ ),
1217
+ /* @__PURE__ */ jsxs11("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-3", children: [
1218
+ /* @__PURE__ */ jsx16(DetailField, { label: t("blog.fields.status"), value: statusLabel }),
1219
+ /* @__PURE__ */ jsx16(DetailField, { label: t("blog.fields.publishedAt"), value: formatDate(post.publishedAt) || "\u2014" }),
1220
+ /* @__PURE__ */ jsx16(
1221
+ DetailField,
1222
+ {
1223
+ label: t("blog.fields.categories"),
1224
+ value: post.categories.map((category) => categoryName(category.id)).filter(Boolean).join(", ") || "\u2014"
1225
+ }
1226
+ )
1227
+ ] }),
1228
+ /* @__PURE__ */ jsx16(BlogPostTranslationFields, { translation })
1229
+ ] });
1230
+ }
1231
+ const isWorking = translation.aiStatus === "pending" || translation.aiStatus === "in_progress";
1232
+ return /* @__PURE__ */ jsxs11("div", { className: "flex w-full flex-col gap-y-6 p-4", children: [
1233
+ /* @__PURE__ */ jsx16(Alert, { children: /* @__PURE__ */ jsxs11(AlertDescription, { className: "flex flex-col gap-y-2", children: [
1234
+ /* @__PURE__ */ jsxs11("div", { className: "flex flex-wrap items-center gap-2", children: [
1235
+ translation.source === "manual" && /* @__PURE__ */ jsx16(Badge, { variant: "softAmber", children: t("blog.translation.editedByHand") }),
1236
+ original && translation.translatedAt && /* @__PURE__ */ jsx16("span", { children: t("blog.translation.translatedFrom", {
1237
+ language: languageName(original.language),
1238
+ date: formatDate(translation.translatedAt)
1239
+ }) })
1240
+ ] }),
1241
+ /* @__PURE__ */ jsx16("span", { children: t("blog.translation.overwriteWarning") }),
1242
+ original && /* @__PURE__ */ jsx16("div", { children: /* @__PURE__ */ jsx16(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => setIsComparing((value) => !value), children: isComparing ? t("blog.actions.hideComparison") : t("blog.actions.compareWithOriginal") }) })
1243
+ ] }) }),
1244
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between gap-x-4", children: [
1245
+ /* @__PURE__ */ jsx16(
1246
+ BlogTranslationStatus,
1247
+ {
1248
+ aiStatus: translation.aiStatus,
1249
+ onRetranslate: async () => {
1250
+ await BlogPostTranslationService.retranslate({ id: translation.id });
1251
+ await reload();
1252
+ }
1253
+ }
1254
+ ),
1255
+ !isWorking && /* @__PURE__ */ jsx16(BlogPostTranslationEditor, { translation, trigger: editTrigger, onSuccess: reload })
1256
+ ] }),
1257
+ isComparing && original ? /* @__PURE__ */ jsx16(BlogPostComparison, { original, translation }) : /* @__PURE__ */ jsx16(BlogPostTranslationFields, { translation })
1258
+ ] });
1259
+ }
1260
+ __name(BlogPostTranslationPanel, "BlogPostTranslationPanel");
1261
+
1262
+ // src/features/blog/components/admin/BlogPostContainer.tsx
1263
+ import { jsx as jsx17 } from "react/jsx-runtime";
1264
+ function BlogPostContainer({ postId }) {
1265
+ return /* @__PURE__ */ jsx17(BlogPostProvider, { postId, children: /* @__PURE__ */ jsx17(BlogPostShell, {}) });
1266
+ }
1267
+ __name(BlogPostContainer, "BlogPostContainer");
1268
+ function BlogPostShell() {
1269
+ const t = useTranslations15();
1270
+ const generateUrl = usePageUrlGenerator();
1271
+ const languageName = useBlogLanguageName();
1272
+ const { post, reload } = useBlogPost();
1273
+ const translations = useMemo9(() => {
1274
+ const all = post?.translations ?? [];
1275
+ return [
1276
+ ...all.filter((translation) => translation.isOriginal),
1277
+ ...all.filter((translation) => !translation.isOriginal)
1278
+ ];
1279
+ }, [post]);
1280
+ if (!post) return null;
1281
+ const title = post.original?.title ?? post.title ?? "";
1282
+ const breadcrumbs = [
1283
+ { name: t("blog.admin.title"), href: generateUrl({ page: Modules.BlogPost }) },
1284
+ { name: title }
1285
+ ];
1286
+ const statusLabel = {
1287
+ draft: t("blog.status.draft"),
1288
+ published: t("blog.status.published"),
1289
+ unpublished: t("blog.status.unpublished")
1290
+ }[post.status];
1291
+ const tabLabel = /* @__PURE__ */ __name((translation) => {
1292
+ const parts = [languageName(translation.language)];
1293
+ if (translation.isOriginal) parts.push(t("blog.translation.original"));
1294
+ if (translation.aiStatus === "pending" || translation.aiStatus === "in_progress")
1295
+ parts.push(t("blog.translation.translating"));
1296
+ if (translation.aiStatus === "failed") parts.push(t("blog.translation.failed"));
1297
+ return parts.join(" \xB7 ");
1298
+ }, "tabLabel");
1299
+ return /* @__PURE__ */ jsx17(
1300
+ SharedProvider,
1301
+ {
1302
+ value: {
1303
+ breadcrumbs,
1304
+ title: {
1305
+ type: t("blog.admin.title"),
1306
+ element: title,
1307
+ functions: [
1308
+ /* @__PURE__ */ jsx17(Badge, { variant: BLOG_STATUS_BADGE[post.status], children: statusLabel }, "blogPostStatus"),
1309
+ /* @__PURE__ */ jsx17(BlogPostPublishButton, {}, "blogPostPublish"),
1310
+ /* @__PURE__ */ jsx17(BlogPostDeleter, {}, "blogPostDeleter")
1311
+ ]
1312
+ }
1313
+ },
1314
+ children: /* @__PURE__ */ jsx17(
1315
+ RoundPageContainer,
1316
+ {
1317
+ module: Modules.BlogPost,
1318
+ id: post.id,
1319
+ fullWidth: true,
1320
+ forceHeader: true,
1321
+ tabs: translations.map((translation) => ({
1322
+ sectionKey: translation.language,
1323
+ label: tabLabel(translation),
1324
+ content: /* @__PURE__ */ jsx17(BlogPostTranslationPanel, { post, translation, onChanged: reload })
1325
+ }))
1326
+ }
1327
+ )
1328
+ }
1329
+ );
1330
+ }
1331
+ __name(BlogPostShell, "BlogPostShell");
1332
+
1333
+ // src/features/blog/components/public/BlogCategoryChips.tsx
1334
+ import { useTranslations as useTranslations16 } from "next-intl";
1335
+
1336
+ // src/features/blog/components/public/blogPublicHelpers.ts
1337
+ function blogListHref(basePath, params) {
1338
+ const search = new URLSearchParams();
1339
+ if (params.category) search.set("category", params.category);
1340
+ if (params.q) search.set("q", params.q);
1341
+ if (params.page && params.page > 1) search.set("page", String(params.page));
1342
+ const query = search.toString();
1343
+ return query ? `${basePath}?${query}` : basePath;
1344
+ }
1345
+ __name(blogListHref, "blogListHref");
1346
+ function blogPostHref(basePath, slug) {
1347
+ return `${basePath}/${encodeURIComponent(slug)}`;
1348
+ }
1349
+ __name(blogPostHref, "blogPostHref");
1350
+ function resolvePostCategory(post, categories) {
1351
+ for (const id of post.categoryIds) {
1352
+ const category = categories.find((c) => c.id === id);
1353
+ if (category) return category;
1354
+ }
1355
+ return void 0;
1356
+ }
1357
+ __name(resolvePostCategory, "resolvePostCategory");
1358
+
1359
+ // src/features/blog/components/public/BlogCategoryChips.tsx
1360
+ import { jsx as jsx18 } from "react/jsx-runtime";
1361
+ function BlogCategoryChips(props) {
1362
+ const { categories, basePath, activeCategorySlug, query, variant = "chips", className } = props;
1363
+ const t = useTranslations16();
1364
+ const items = [
1365
+ {
1366
+ key: "all",
1367
+ label: t("blog.public.all"),
1368
+ href: blogListHref(basePath, { q: query }),
1369
+ active: !activeCategorySlug
1370
+ },
1371
+ ...categories.map((category) => ({
1372
+ key: category.id,
1373
+ label: category.name,
1374
+ href: blogListHref(basePath, { category: category.slug, q: query }),
1375
+ active: category.slug === activeCategorySlug
1376
+ }))
1377
+ ];
1378
+ const inactiveVariant = variant === "chips" ? "outline" : "ghost";
1379
+ return /* @__PURE__ */ jsx18("nav", { "aria-label": t("blog.entities.categories"), className, children: /* @__PURE__ */ jsx18("ul", { className: variant === "chips" ? "flex flex-wrap items-center gap-2" : "flex flex-col items-start gap-2", children: items.map((item) => /* @__PURE__ */ jsx18("li", { children: /* @__PURE__ */ jsx18(
1380
+ Badge,
1381
+ {
1382
+ variant: item.active ? "default" : inactiveVariant,
1383
+ render: /* @__PURE__ */ jsx18(Link, { href: item.href, "aria-current": item.active ? "page" : void 0 }),
1384
+ children: item.label
1385
+ }
1386
+ ) }, item.key)) }) });
1387
+ }
1388
+ __name(BlogCategoryChips, "BlogCategoryChips");
1389
+
1390
+ // src/features/blog/components/public/BlogListParts.tsx
1391
+ import { useTranslations as useTranslations17 } from "next-intl";
1392
+ import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
1393
+ function BlogListHeader(props) {
1394
+ const { align = "start", showSubtitle = true } = props;
1395
+ const t = useTranslations17();
1396
+ return /* @__PURE__ */ jsxs12("header", { className: align === "center" ? "text-center" : void 0, children: [
1397
+ /* @__PURE__ */ jsx19(
1398
+ ContentTitle,
1399
+ {
1400
+ type: t("blog.public.eyebrow"),
1401
+ element: t("blog.public.title"),
1402
+ headingLevel: 1,
1403
+ className: cn("mb-0", align === "center" && "[&_div]:justify-center")
1404
+ }
1405
+ ),
1406
+ showSubtitle ? /* @__PURE__ */ jsx19("p", { className: "text-muted-foreground mt-3 text-sm leading-relaxed", children: t("blog.public.subtitle") }) : null
1407
+ ] });
1408
+ }
1409
+ __name(BlogListHeader, "BlogListHeader");
1410
+ function BlogListEmpty(props) {
1411
+ const t = useTranslations17();
1412
+ return /* @__PURE__ */ jsx19("p", { className: cn("text-muted-foreground py-12 text-center text-sm", props.className), children: props.query ? t("blog.public.noResults") : t("blog.public.empty") });
1413
+ }
1414
+ __name(BlogListEmpty, "BlogListEmpty");
1415
+ function BlogLoadMore(props) {
1416
+ const { basePath, page, activeCategorySlug, query, className } = props;
1417
+ const t = useTranslations17();
1418
+ return /* @__PURE__ */ jsx19("div", { className: cn("flex justify-center", className), children: /* @__PURE__ */ jsx19(
1419
+ Link,
1420
+ {
1421
+ href: blogListHref(basePath, { category: activeCategorySlug, q: query, page: page + 1 }),
1422
+ scroll: false,
1423
+ className: buttonVariants({ variant: "outline" }),
1424
+ children: t("blog.public.loadMore")
1425
+ }
1426
+ ) });
1427
+ }
1428
+ __name(BlogLoadMore, "BlogLoadMore");
1429
+
1430
+ // src/features/blog/components/public/BlogPostCard.tsx
1431
+ import { useFormatter as useFormatter2 } from "next-intl";
1432
+ import Image3 from "next/image";
1433
+
1434
+ // src/features/blog/components/public/BlogPostMeta.tsx
1435
+ import { useFormatter, useTranslations as useTranslations18 } from "next-intl";
1436
+ import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
1437
+ var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
1438
+ function BlogPostMeta(props) {
1439
+ const { post, showAuthor, showDate, showUpdated, className } = props;
1440
+ const t = useTranslations18();
1441
+ const format = useFormatter();
1442
+ const parts = [];
1443
+ if (showAuthor && post.authorName) parts.push(t("blog.public.by", { author: post.authorName }));
1444
+ if (showDate && post.publishedAt) {
1445
+ parts.push(format.dateTime(new Date(post.publishedAt), { dateStyle: "medium" }));
1446
+ }
1447
+ if (showUpdated && post.publishedAt && post.updatedAt && new Date(post.updatedAt).getTime() - new Date(post.publishedAt).getTime() > ONE_DAY_MS) {
1448
+ parts.push(t("blog.public.updated", { date: format.dateTime(new Date(post.updatedAt), { dateStyle: "medium" }) }));
1449
+ }
1450
+ parts.push(t("blog.public.readingTime", { minutes: post.readingMinutes }));
1451
+ return /* @__PURE__ */ jsx20("p", { className: cn("text-muted-foreground text-xs", className), children: parts.map((part, index) => /* @__PURE__ */ jsxs13("span", { children: [
1452
+ index > 0 ? " \xB7 " : null,
1453
+ part
1454
+ ] }, index)) });
1455
+ }
1456
+ __name(BlogPostMeta, "BlogPostMeta");
1457
+
1458
+ // src/features/blog/components/public/BlogPostCard.tsx
1459
+ import { jsx as jsx21, jsxs as jsxs14 } from "react/jsx-runtime";
1460
+ function BlogPostCover(props) {
1461
+ const { src, alt, sizes, priority, className } = props;
1462
+ return /* @__PURE__ */ jsx21("div", { className: cn("border-border bg-primary/10 relative overflow-hidden rounded-lg border", className), children: src ? /* @__PURE__ */ jsx21(
1463
+ Image3,
1464
+ {
1465
+ src,
1466
+ alt: alt ?? "",
1467
+ fill: true,
1468
+ sizes,
1469
+ priority,
1470
+ className: "object-cover",
1471
+ unoptimized: isRemoteImageSrc(src)
1472
+ }
1473
+ ) : null });
1474
+ }
1475
+ __name(BlogPostCover, "BlogPostCover");
1476
+ function BlogPostCard(props) {
1477
+ const { post, categories, basePath, variant = "card", priority, headingLevel = "h2" } = props;
1478
+ const format = useFormatter2();
1479
+ const category = resolvePostCategory(post, categories);
1480
+ const isProminent = variant === "hero" || variant === "large";
1481
+ const eyebrow = variant === "row" && post.publishedAt ? [category?.name, format.dateTime(new Date(post.publishedAt), { day: "numeric", month: "short" })].filter(Boolean).join(" \xB7 ") : category?.name;
1482
+ const text = /* @__PURE__ */ jsxs14("div", { className: "flex min-w-0 flex-col", children: [
1483
+ eyebrow ? /* @__PURE__ */ jsx21(MicroLabel, { as: "span", children: eyebrow }) : null,
1484
+ /* @__PURE__ */ jsx21(SectionHeader, { level: headingLevel === "h3" ? 3 : 2, className: "mt-1.5", children: /* @__PURE__ */ jsx21(Link, { href: blogPostHref(basePath, post.slug), className: "after:absolute after:inset-0", children: post.title }) }),
1485
+ post.summary ? /* @__PURE__ */ jsx21("p", { className: "text-muted-foreground mt-2 text-sm leading-relaxed", children: post.summary }) : null,
1486
+ /* @__PURE__ */ jsx21(BlogPostMeta, { post, showAuthor: isProminent, showDate: isProminent, className: "mt-3" })
1487
+ ] });
1488
+ if (variant === "hero") {
1489
+ return /* @__PURE__ */ jsxs14("article", { className: "group relative grid items-center gap-6 md:grid-cols-[1.4fr_1fr] md:gap-10", children: [
1490
+ /* @__PURE__ */ jsx21(
1491
+ BlogPostCover,
1492
+ {
1493
+ src: post.coverImage,
1494
+ alt: post.coverImageAlt,
1495
+ sizes: "(min-width: 1024px) 560px, (min-width: 768px) 58vw, 100vw",
1496
+ priority,
1497
+ className: "aspect-[16/10]"
1498
+ }
1499
+ ),
1500
+ text
1501
+ ] });
1502
+ }
1503
+ if (variant === "row") {
1504
+ return /* @__PURE__ */ jsxs14("article", { className: "group relative grid items-center gap-4 sm:grid-cols-[10rem_1fr] sm:gap-6", children: [
1505
+ /* @__PURE__ */ jsx21(
1506
+ BlogPostCover,
1507
+ {
1508
+ src: post.coverImage,
1509
+ alt: post.coverImageAlt,
1510
+ sizes: "(min-width: 640px) 160px, 100vw",
1511
+ priority,
1512
+ className: "aspect-[16/10]"
1513
+ }
1514
+ ),
1515
+ text
1516
+ ] });
1517
+ }
1518
+ return /* @__PURE__ */ jsxs14("article", { className: "group relative flex flex-col gap-4", children: [
1519
+ /* @__PURE__ */ jsx21(
1520
+ BlogPostCover,
1521
+ {
1522
+ src: post.coverImage,
1523
+ alt: post.coverImageAlt,
1524
+ sizes: variant === "large" ? "(min-width: 1024px) 760px, 100vw" : "(min-width: 1024px) 320px, (min-width: 640px) 50vw, 100vw",
1525
+ priority,
1526
+ className: variant === "large" ? "aspect-[16/8]" : "aspect-[16/10]"
1527
+ }
1528
+ ),
1529
+ text
1530
+ ] });
1531
+ }
1532
+ __name(BlogPostCard, "BlogPostCard");
1533
+
1534
+ // src/features/blog/components/public/BlogSearchBox.tsx
1535
+ import { SearchIcon } from "lucide-react";
1536
+ import { useTranslations as useTranslations19 } from "next-intl";
1537
+ import { useEffect as useEffect5, useState as useState11 } from "react";
1538
+ import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
1539
+ function BlogSearchBox(props) {
1540
+ const { basePath, query, activeCategorySlug, className } = props;
1541
+ const t = useTranslations19();
1542
+ const router = useI18nRouter();
1543
+ const [value, setValue] = useState11(query ?? "");
1544
+ useEffect5(() => setValue(query ?? ""), [query]);
1545
+ const onSubmit = /* @__PURE__ */ __name((event) => {
1546
+ event.preventDefault();
1547
+ const q = value.trim();
1548
+ router.push(blogListHref(basePath, { category: activeCategorySlug, q: q || void 0 }));
1549
+ }, "onSubmit");
1550
+ return /* @__PURE__ */ jsx22("form", { role: "search", onSubmit, className, children: /* @__PURE__ */ jsxs15(InputGroup, { children: [
1551
+ /* @__PURE__ */ jsx22(InputGroupAddon, { children: /* @__PURE__ */ jsx22(SearchIcon, {}) }),
1552
+ /* @__PURE__ */ jsx22(
1553
+ InputGroupInput,
1554
+ {
1555
+ type: "search",
1556
+ name: "q",
1557
+ value,
1558
+ onChange: (event) => setValue(event.target.value),
1559
+ placeholder: t("blog.public.searchPlaceholder"),
1560
+ "aria-label": t("blog.public.search")
1561
+ }
1562
+ )
1563
+ ] }) });
1564
+ }
1565
+ __name(BlogSearchBox, "BlogSearchBox");
1566
+
1567
+ // src/features/blog/components/public/BlogListColumn.tsx
1568
+ import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
1569
+ function BlogListColumn(props) {
1570
+ const { list, categories, activeCategorySlug, query, page, basePath } = props;
1571
+ return /* @__PURE__ */ jsx23("section", { className: "px-4 pt-16 pb-20 md:pt-20", children: /* @__PURE__ */ jsxs16("div", { className: "mx-auto max-w-2xl", children: [
1572
+ /* @__PURE__ */ jsx23(BlogListHeader, { align: "center" }),
1573
+ /* @__PURE__ */ jsx23(
1574
+ BlogCategoryChips,
1575
+ {
1576
+ categories,
1577
+ basePath,
1578
+ activeCategorySlug,
1579
+ query,
1580
+ className: "mt-8 [&>ul]:justify-center"
1581
+ }
1582
+ ),
1583
+ /* @__PURE__ */ jsx23(BlogSearchBox, { basePath, query, activeCategorySlug, className: "mt-4" }),
1584
+ list.posts.length === 0 ? /* @__PURE__ */ jsx23(BlogListEmpty, { query }) : /* @__PURE__ */ jsx23("div", { className: "divide-border mt-10 flex flex-col divide-y", children: list.posts.map((post, index) => /* @__PURE__ */ jsx23("div", { className: "py-6 first:pt-0", children: /* @__PURE__ */ jsx23(
1585
+ BlogPostCard,
1586
+ {
1587
+ post,
1588
+ categories,
1589
+ basePath,
1590
+ variant: "row",
1591
+ priority: index === 0
1592
+ }
1593
+ ) }, post.id)) }),
1594
+ list.hasMore ? /* @__PURE__ */ jsx23(
1595
+ BlogLoadMore,
1596
+ {
1597
+ basePath,
1598
+ page,
1599
+ activeCategorySlug,
1600
+ query,
1601
+ className: "mt-10"
1602
+ }
1603
+ ) : null
1604
+ ] }) });
1605
+ }
1606
+ __name(BlogListColumn, "BlogListColumn");
1607
+
1608
+ // src/features/blog/components/public/BlogListFeatured.tsx
1609
+ import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
1610
+ function BlogListFeatured(props) {
1611
+ const { list, categories, activeCategorySlug, query, page, basePath } = props;
1612
+ const [hero, ...rest] = list.posts;
1613
+ return /* @__PURE__ */ jsx24("section", { className: "px-4 pt-16 pb-20 md:pt-20", children: /* @__PURE__ */ jsxs17("div", { className: "mx-auto max-w-5xl", children: [
1614
+ /* @__PURE__ */ jsx24(BlogListHeader, {}),
1615
+ hero ? /* @__PURE__ */ jsx24("div", { className: "mt-10", children: /* @__PURE__ */ jsx24(BlogPostCard, { post: hero, categories, basePath, variant: "hero", priority: true }) }) : null,
1616
+ /* @__PURE__ */ jsxs17("div", { className: "mt-12 flex flex-col gap-3 md:flex-row md:items-center", children: [
1617
+ /* @__PURE__ */ jsx24(
1618
+ BlogCategoryChips,
1619
+ {
1620
+ categories,
1621
+ basePath,
1622
+ activeCategorySlug,
1623
+ query,
1624
+ className: "flex-1"
1625
+ }
1626
+ ),
1627
+ /* @__PURE__ */ jsx24(
1628
+ BlogSearchBox,
1629
+ {
1630
+ basePath,
1631
+ query,
1632
+ activeCategorySlug,
1633
+ className: "md:w-64"
1634
+ }
1635
+ )
1636
+ ] }),
1637
+ list.posts.length === 0 ? /* @__PURE__ */ jsx24(BlogListEmpty, { query }) : null,
1638
+ rest.length > 0 ? /* @__PURE__ */ jsx24("div", { className: "mt-8 grid gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-3", children: rest.map((post) => /* @__PURE__ */ jsx24(BlogPostCard, { post, categories, basePath }, post.id)) }) : null,
1639
+ list.hasMore ? /* @__PURE__ */ jsx24(
1640
+ BlogLoadMore,
1641
+ {
1642
+ basePath,
1643
+ page,
1644
+ activeCategorySlug,
1645
+ query,
1646
+ className: "mt-12"
1647
+ }
1648
+ ) : null
1649
+ ] }) });
1650
+ }
1651
+ __name(BlogListFeatured, "BlogListFeatured");
1652
+
1653
+ // src/features/blog/components/public/BlogListRail.tsx
1654
+ import { useTranslations as useTranslations20 } from "next-intl";
1655
+ import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
1656
+ function BlogListRail(props) {
1657
+ const { list, categories, activeCategorySlug, query, page, basePath } = props;
1658
+ const t = useTranslations20();
1659
+ const [first, ...rest] = list.posts;
1660
+ return /* @__PURE__ */ jsx25("section", { className: "px-4 pt-16 pb-20 md:pt-20", children: /* @__PURE__ */ jsxs18("div", { className: "mx-auto grid max-w-5xl gap-10 md:grid-cols-[13rem_1fr] md:gap-12", children: [
1661
+ /* @__PURE__ */ jsxs18("aside", { className: "flex flex-col gap-6", children: [
1662
+ /* @__PURE__ */ jsx25(BlogListHeader, { showSubtitle: false }),
1663
+ /* @__PURE__ */ jsx25(BlogSearchBox, { basePath, query, activeCategorySlug }),
1664
+ /* @__PURE__ */ jsxs18("div", { children: [
1665
+ /* @__PURE__ */ jsx25(MicroLabel, { as: "span", className: "block", children: t("blog.entities.categories") }),
1666
+ /* @__PURE__ */ jsx25(
1667
+ BlogCategoryChips,
1668
+ {
1669
+ categories,
1670
+ basePath,
1671
+ activeCategorySlug,
1672
+ query,
1673
+ variant: "list",
1674
+ className: "mt-3"
1675
+ }
1676
+ )
1677
+ ] })
1678
+ ] }),
1679
+ /* @__PURE__ */ jsxs18("div", { className: "min-w-0", children: [
1680
+ first ? /* @__PURE__ */ jsx25(BlogPostCard, { post: first, categories, basePath, variant: "large", priority: true }) : /* @__PURE__ */ jsx25(BlogListEmpty, { query }),
1681
+ rest.length > 0 ? /* @__PURE__ */ jsx25("div", { className: "border-border mt-10 grid gap-x-6 gap-y-10 border-t pt-10 sm:grid-cols-2", children: rest.map((post) => /* @__PURE__ */ jsx25(BlogPostCard, { post, categories, basePath }, post.id)) }) : null,
1682
+ list.hasMore ? /* @__PURE__ */ jsx25(
1683
+ BlogLoadMore,
1684
+ {
1685
+ basePath,
1686
+ page,
1687
+ activeCategorySlug,
1688
+ query,
1689
+ className: "mt-12"
1690
+ }
1691
+ ) : null
1692
+ ] })
1693
+ ] }) });
1694
+ }
1695
+ __name(BlogListRail, "BlogListRail");
1696
+
1697
+ // src/features/blog/components/public/BlogList.tsx
1698
+ import { jsx as jsx26 } from "react/jsx-runtime";
1699
+ function BlogList(props) {
1700
+ switch (props.layout) {
1701
+ case "column":
1702
+ return /* @__PURE__ */ jsx26(BlogListColumn, { ...props });
1703
+ case "rail":
1704
+ return /* @__PURE__ */ jsx26(BlogListRail, { ...props });
1705
+ case "featured":
1706
+ default:
1707
+ return /* @__PURE__ */ jsx26(BlogListFeatured, { ...props });
1708
+ }
1709
+ }
1710
+ __name(BlogList, "BlogList");
1711
+
1712
+ // src/features/blog/components/public/BlogPostView.tsx
1713
+ import { ArrowLeftIcon } from "lucide-react";
1714
+ import { useTranslations as useTranslations21 } from "next-intl";
1715
+ import { jsx as jsx27, jsxs as jsxs19 } from "react/jsx-runtime";
1716
+ function BlogPostView(props) {
1717
+ const { post, categories, related, basePath } = props;
1718
+ const t = useTranslations21();
1719
+ const category = resolvePostCategory(post, categories);
1720
+ const more = related.slice(0, 3);
1721
+ return /* @__PURE__ */ jsxs19("article", { className: "px-4 pt-12 pb-20 md:pt-16", children: [
1722
+ /* @__PURE__ */ jsxs19("div", { className: "mx-auto max-w-3xl", children: [
1723
+ /* @__PURE__ */ jsxs19(Link, { href: basePath, className: "inline-flex items-center gap-1.5", children: [
1724
+ /* @__PURE__ */ jsx27(ArrowLeftIcon, { className: "size-4 rtl:rotate-180" }),
1725
+ t("blog.public.back")
1726
+ ] }),
1727
+ /* @__PURE__ */ jsxs19("header", { className: "mt-8", children: [
1728
+ /* @__PURE__ */ jsx27(ContentTitle, { type: category?.name, element: post.title, headingLevel: 1, className: "mb-0" }),
1729
+ post.summary ? /* @__PURE__ */ jsx27("p", { className: "text-muted-foreground mt-3 text-sm leading-relaxed", children: post.summary }) : null,
1730
+ /* @__PURE__ */ jsx27(BlogPostMeta, { post, showAuthor: true, showDate: true, showUpdated: true, className: "mt-4" })
1731
+ ] }),
1732
+ post.coverImage ? /* @__PURE__ */ jsx27(
1733
+ BlogPostCover,
1734
+ {
1735
+ src: post.coverImage,
1736
+ alt: post.coverImageAlt,
1737
+ sizes: "(min-width: 768px) 768px, 100vw",
1738
+ priority: true,
1739
+ className: "mt-8 aspect-[16/9]"
1740
+ }
1741
+ ) : null,
1742
+ /* @__PURE__ */ jsx27("div", { className: "mt-8", children: /* @__PURE__ */ jsx27(BlockNoteEditorContainer, { id: post.id, type: "blog-post", initialContent: post.body }) })
1743
+ ] }),
1744
+ more.length > 0 ? /* @__PURE__ */ jsx27("section", { className: "mx-auto mt-16 max-w-5xl", children: /* @__PURE__ */ jsxs19("div", { className: "border-border border-t pt-10", children: [
1745
+ /* @__PURE__ */ jsx27(SectionHeader, { level: 2, children: t("blog.public.moreIn", { category: category?.name ?? "" }) }),
1746
+ /* @__PURE__ */ jsx27("div", { className: "mt-6 grid gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-3", children: more.map((relatedPost) => /* @__PURE__ */ jsx27(
1747
+ BlogPostCard,
1748
+ {
1749
+ post: relatedPost,
1750
+ categories,
1751
+ basePath,
1752
+ headingLevel: "h3"
1753
+ },
1754
+ relatedPost.id
1755
+ )) })
1756
+ ] }) }) : null
1757
+ ] });
1758
+ }
1759
+ __name(BlogPostView, "BlogPostView");
1760
+ export {
1761
+ BLOG_ACTION,
1762
+ BLOG_CATEGORY_UPLOAD_PREFIX,
1763
+ BLOG_LIST_LAYOUTS,
1764
+ BLOG_MODULE_ID,
1765
+ BLOG_POST_STATUSES,
1766
+ BLOG_POST_UPLOAD_PREFIX,
1767
+ BLOG_PUBLIC_PREFIX,
1768
+ BLOG_SETTINGS_ID,
1769
+ BLOG_STATUS_BADGE,
1770
+ BLOG_TRANSLATION_SOURCES,
1771
+ BLOG_UPLOAD_ROOT,
1772
+ BlogAdminContainer,
1773
+ BlogCategoriesList,
1774
+ BlogCategoryEditor,
1775
+ BlogCoverImageField,
1776
+ BlogLayoutSetting,
1777
+ BlogList,
1778
+ BlogPostComparison,
1779
+ BlogPostContainer,
1780
+ BlogPostDeleter,
1781
+ BlogPostEditor,
1782
+ BlogPostProvider,
1783
+ BlogPostPublishButton,
1784
+ BlogPostTranslationEditor,
1785
+ BlogPostTranslationFields,
1786
+ BlogPostTranslationPanel,
1787
+ BlogPostView,
1788
+ BlogPostsList,
1789
+ BlogTranslationStatus,
1790
+ blogCategoryAdminName,
1791
+ uploadBlogImage,
1792
+ useBlogCategoryNames,
1793
+ useBlogCategoryTableStructure,
1794
+ useBlogDateFormatter,
1795
+ useBlogLanguageName,
1796
+ useBlogPost,
1797
+ useBlogPostTableStructure
1798
+ };
1799
+ //# sourceMappingURL=index.mjs.map