@carlonicora/nextjs-jsonapi 5.2.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BlockNoteEditor-EGY4ZAAM.js → BlockNoteEditor-4YCAIEPT.js} +23 -22
- package/dist/BlockNoteEditor-4YCAIEPT.js.map +1 -0
- package/dist/{BlockNoteEditor-2QJVK2HH.mjs → BlockNoteEditor-YQL2SGFR.mjs} +8 -7
- package/dist/BlockNoteEditor-YQL2SGFR.mjs.map +1 -0
- package/dist/{BlockNoteViewer-6BQXJ4TB.mjs → BlockNoteViewer-AEJKJ27Q.mjs} +2 -2
- package/dist/{BlockNoteViewer-I2MMFH3S.js → BlockNoteViewer-YL3J7J2Y.js} +3 -3
- package/dist/{BlockNoteViewer-I2MMFH3S.js.map → BlockNoteViewer-YL3J7J2Y.js.map} +1 -1
- package/dist/BlogCategoryFields-DO48vOTE.d.mts +14 -0
- package/dist/BlogCategoryFields-DO48vOTE.d.ts +14 -0
- package/dist/BlogPostInterface-BB_4wcDR.d.mts +124 -0
- package/dist/BlogPostInterface-bWAT1rfR.d.ts +124 -0
- package/dist/billing/index.js +441 -441
- package/dist/billing/index.mjs +3 -3
- package/dist/blog-public.types-BbrzsC1c.d.mts +30 -0
- package/dist/blog-public.types-BbrzsC1c.d.ts +30 -0
- package/dist/{chunk-3VEQ5JZJ.js → chunk-3NLAXVIN.js} +860 -849
- package/dist/{chunk-3VEQ5JZJ.js.map → chunk-3NLAXVIN.js.map} +1 -1
- package/dist/{chunk-YMUPTXOT.mjs → chunk-3YW35YHQ.mjs} +1 -101
- package/dist/chunk-3YW35YHQ.mjs.map +1 -0
- package/dist/{chunk-EXICQC4A.js → chunk-5PPG6PL7.js} +3 -3
- package/dist/{chunk-EXICQC4A.js.map → chunk-5PPG6PL7.js.map} +1 -1
- package/dist/{chunk-NPKFMRH5.mjs → chunk-6DC2WVVB.mjs} +2 -2
- package/dist/{chunk-2Z3ZCYXX.mjs → chunk-EUKTBOI2.mjs} +19 -8
- package/dist/chunk-EUKTBOI2.mjs.map +1 -0
- package/dist/{chunk-THKYH65N.js → chunk-KSYQEKJX.js} +11 -111
- package/dist/chunk-KSYQEKJX.js.map +1 -0
- package/dist/{chunk-S5MQDWWC.js → chunk-NFBOSPNJ.js} +1451 -176
- package/dist/chunk-NFBOSPNJ.js.map +1 -0
- package/dist/{chunk-JEZQTZQ4.mjs → chunk-QMYQRPQC.mjs} +2 -2
- package/dist/{chunk-HNOWSFM5.mjs → chunk-WC63PJSC.mjs} +1278 -3
- package/dist/chunk-WC63PJSC.mjs.map +1 -0
- package/dist/chunk-XCHSX3VC.mjs +107 -0
- package/dist/chunk-XCHSX3VC.mjs.map +1 -0
- package/dist/{chunk-75MLT74B.js → chunk-YNCQSWQW.js} +8 -8
- package/dist/{chunk-75MLT74B.js.map → chunk-YNCQSWQW.js.map} +1 -1
- package/dist/chunk-YW4ZCEWB.js +107 -0
- package/dist/chunk-YW4ZCEWB.js.map +1 -0
- package/dist/client/index.d.mts +6 -39
- package/dist/client/index.d.ts +6 -39
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +15 -4
- package/dist/components/index.d.ts +15 -4
- package/dist/components/index.js +4 -4
- package/dist/components/index.mjs +3 -3
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +400 -3
- package/dist/core/index.d.ts +400 -3
- package/dist/core/index.js +64 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +63 -1
- package/dist/email-automation-enrollment.fields-BZvF14qx.d.mts +82 -0
- package/dist/email-automation-enrollment.fields-Cp3VMzFq.d.ts +82 -0
- package/dist/email-automations/index.css +30 -0
- package/dist/email-automations/index.css.map +1 -0
- package/dist/email-automations/index.d.mts +303 -0
- package/dist/email-automations/index.d.ts +303 -0
- package/dist/email-automations/index.js +1705 -0
- package/dist/email-automations/index.js.map +1 -0
- package/dist/email-automations/index.mjs +1705 -0
- package/dist/email-automations/index.mjs.map +1 -0
- package/dist/features/analytics/index.js +122 -122
- package/dist/features/analytics/index.mjs +4 -4
- package/dist/features/analytics/server.js +6 -5
- package/dist/features/analytics/server.js.map +1 -1
- package/dist/features/analytics/server.mjs +4 -3
- package/dist/features/analytics/server.mjs.map +1 -1
- package/dist/features/blog/index.css +30 -0
- package/dist/features/blog/index.css.map +1 -0
- package/dist/features/blog/index.d.mts +266 -0
- package/dist/features/blog/index.d.ts +266 -0
- package/dist/features/blog/index.js +1799 -0
- package/dist/features/blog/index.js.map +1 -0
- package/dist/features/blog/index.mjs +1799 -0
- package/dist/features/blog/index.mjs.map +1 -0
- package/dist/features/blog/server.d.mts +33 -0
- package/dist/features/blog/server.d.ts +33 -0
- package/dist/features/blog/server.js +139 -0
- package/dist/features/blog/server.js.map +1 -0
- package/dist/features/blog/server.mjs +139 -0
- package/dist/features/blog/server.mjs.map +1 -0
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/tokenusage/index.js +125 -125
- package/dist/features/tokenusage/index.mjs +4 -4
- package/dist/index.d.mts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +65 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +64 -2
- package/dist/{s3.service-CY-jgOAM.d.ts → s3.service-Bz_K-tjz.d.ts} +11 -0
- package/dist/{s3.service-Sri0QP7Z.d.mts → s3.service-D5zNCEW3.d.mts} +11 -0
- package/dist/server/index.d.mts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +8 -6
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +12 -10
- package/dist/types-70acd-8C.d.mts +39 -0
- package/dist/types-l8SFleXi.d.ts +39 -0
- package/messages/en.json +210 -0
- package/messages/it.json +210 -0
- package/package.json +21 -1
- package/src/components/editors/BlockNoteEditor.tsx +13 -3
- package/src/components/forms/FormBlockNote.tsx +4 -0
- package/src/components/forms/FormFieldWrapper.tsx +1 -1
- package/src/components/forms/FormTextarea.tsx +2 -1
- package/src/components/navigations/ContentTitle.tsx +14 -3
- package/src/core/index.ts +12 -0
- package/src/core/registry/ModuleRegistry.ts +11 -0
- package/src/email-automations/index.ts +5 -0
- package/src/features/blog/BlogModules.ts +49 -0
- package/src/features/blog/components/admin/BlogAdminContainer.tsx +32 -0
- package/src/features/blog/components/admin/BlogCategoriesList.tsx +56 -0
- package/src/features/blog/components/admin/BlogCategoryEditor.tsx +230 -0
- package/src/features/blog/components/admin/BlogCoverImageField.tsx +123 -0
- package/src/features/blog/components/admin/BlogLayoutSetting.tsx +86 -0
- package/src/features/blog/components/admin/BlogPostComparison.tsx +54 -0
- package/src/features/blog/components/admin/BlogPostContainer.tsx +102 -0
- package/src/features/blog/components/admin/BlogPostDeleter.tsx +35 -0
- package/src/features/blog/components/admin/BlogPostEditor.tsx +207 -0
- package/src/features/blog/components/admin/BlogPostPublishButton.tsx +50 -0
- package/src/features/blog/components/admin/BlogPostTranslationEditor.tsx +110 -0
- package/src/features/blog/components/admin/BlogPostTranslationPanel.tsx +136 -0
- package/src/features/blog/components/admin/BlogPostsList.tsx +58 -0
- package/src/features/blog/components/admin/BlogTranslationStatus.tsx +104 -0
- package/src/features/blog/components/admin/__tests__/BlogPostTranslationPanel.spec.tsx +100 -0
- package/src/features/blog/components/admin/__tests__/BlogPostsList.spec.tsx +30 -0
- package/src/features/blog/components/admin/index.ts +14 -0
- package/src/features/blog/components/public/BlogCategoryChips.tsx +59 -0
- package/src/features/blog/components/public/BlogList.tsx +21 -0
- package/src/features/blog/components/public/BlogListColumn.tsx +53 -0
- package/src/features/blog/components/public/BlogListFeatured.tsx +57 -0
- package/src/features/blog/components/public/BlogListParts.tsx +74 -0
- package/src/features/blog/components/public/BlogListRail.tsx +62 -0
- package/src/features/blog/components/public/BlogPostCard.tsx +132 -0
- package/src/features/blog/components/public/BlogPostMeta.tsx +49 -0
- package/src/features/blog/components/public/BlogPostView.tsx +74 -0
- package/src/features/blog/components/public/BlogSearchBox.tsx +52 -0
- package/src/features/blog/components/public/__tests__/BlogList.spec.tsx +173 -0
- package/src/features/blog/components/public/__tests__/BlogPostView.spec.tsx +128 -0
- package/src/features/blog/components/public/blogPublicHelpers.ts +30 -0
- package/src/features/blog/components/public/index.ts +3 -0
- package/src/features/blog/contexts/BlogPostContext.tsx +66 -0
- package/src/features/blog/contexts/index.ts +1 -0
- package/src/features/blog/data/BlogCategory.ts +93 -0
- package/src/features/blog/data/BlogCategoryFields.ts +4 -0
- package/src/features/blog/data/BlogCategoryInterface.ts +24 -0
- package/src/features/blog/data/BlogCategoryService.ts +54 -0
- package/src/features/blog/data/BlogCategoryTranslation.ts +90 -0
- package/src/features/blog/data/BlogCategoryTranslationInterface.ts +22 -0
- package/src/features/blog/data/BlogCategoryTranslationService.ts +30 -0
- package/src/features/blog/data/BlogPost.spec.ts +123 -0
- package/src/features/blog/data/BlogPost.ts +159 -0
- package/src/features/blog/data/BlogPostFields.ts +7 -0
- package/src/features/blog/data/BlogPostInterface.ts +39 -0
- package/src/features/blog/data/BlogPostService.ts +82 -0
- package/src/features/blog/data/BlogPostTranslation.spec.ts +73 -0
- package/src/features/blog/data/BlogPostTranslation.ts +110 -0
- package/src/features/blog/data/BlogPostTranslationInterface.ts +27 -0
- package/src/features/blog/data/BlogPostTranslationService.ts +30 -0
- package/src/features/blog/data/BlogSettings.ts +37 -0
- package/src/features/blog/data/BlogSettingsInterface.ts +11 -0
- package/src/features/blog/data/BlogSettingsService.ts +24 -0
- package/src/features/blog/data/index.ts +17 -0
- package/src/features/blog/hooks/index.ts +3 -0
- package/src/features/blog/hooks/useBlogCategoryNames.ts +34 -0
- package/src/features/blog/hooks/useBlogCategoryTableStructure.tsx +84 -0
- package/src/features/blog/hooks/useBlogPostTableStructure.tsx +126 -0
- package/src/features/blog/index.ts +15 -0
- package/src/features/blog/server/fetchers.ts +120 -0
- package/src/features/blog/server/toPublic.spec.ts +91 -0
- package/src/features/blog/server/toPublic.ts +36 -0
- package/src/features/blog/server-entry.ts +17 -0
- package/src/features/blog/types/blog-public.types.ts +34 -0
- package/src/features/blog/types/blog.constants.ts +40 -0
- package/src/features/email-automation/EmailAutomationEnrollmentModule.ts +11 -0
- package/src/features/email-automation/EmailAutomationModule.ts +30 -0
- package/src/features/email-automation/EmailAutomationStepModule.ts +11 -0
- package/src/features/email-automation/EmailAutomationTriggerModule.ts +11 -0
- package/src/features/email-automation/components/__tests__/EmailAutomationEditor.spec.tsx +104 -0
- package/src/features/email-automation/components/canvas/EmailAutomationActiveSwitch.tsx +49 -0
- package/src/features/email-automation/components/canvas/EmailAutomationCanvas.tsx +474 -0
- package/src/features/email-automation/components/canvas/EmailAutomationCanvasInspector.tsx +89 -0
- package/src/features/email-automation/components/canvas/EmailAutomationCanvasTab.tsx +32 -0
- package/src/features/email-automation/components/canvas/__tests__/buildCanvasGraph.spec.ts +67 -0
- package/src/features/email-automation/components/canvas/buildCanvasGraph.ts +141 -0
- package/src/features/email-automation/components/canvas/edges/DelayEdge.tsx +68 -0
- package/src/features/email-automation/components/canvas/nodes/EmailStepNode.tsx +43 -0
- package/src/features/email-automation/components/canvas/nodes/TriggerNode.tsx +23 -0
- package/src/features/email-automation/components/canvas/shared/CanvasToolbar.tsx +53 -0
- package/src/features/email-automation/components/canvas/shared/EmptyCanvas.tsx +11 -0
- package/src/features/email-automation/components/canvas/shared/__tests__/autoLayout.spec.ts +42 -0
- package/src/features/email-automation/components/canvas/shared/__tests__/longPress.spec.ts +51 -0
- package/src/features/email-automation/components/canvas/shared/autoLayout.ts +55 -0
- package/src/features/email-automation/components/canvas/shared/index.ts +4 -0
- package/src/features/email-automation/components/canvas/shared/longPress.ts +57 -0
- package/src/features/email-automation/components/containers/EmailAutomationContainer.tsx +80 -0
- package/src/features/email-automation/components/containers/EmailAutomationsListContainer.tsx +27 -0
- package/src/features/email-automation/components/forms/EmailAutomationEditor.tsx +153 -0
- package/src/features/email-automation/components/forms/EmailStepEditor.tsx +151 -0
- package/src/features/email-automation/components/index.ts +15 -0
- package/src/features/email-automation/components/lists/EmailAutomationRecipientsList.tsx +90 -0
- package/src/features/email-automation/components/lists/EmailAutomationsList.tsx +76 -0
- package/src/features/email-automation/components/unsubscribe/EmailUnsubscribe.tsx +99 -0
- package/src/features/email-automation/components/unsubscribe/__tests__/EmailUnsubscribe.spec.tsx +55 -0
- package/src/features/email-automation/contexts/EmailAutomationContext.tsx +175 -0
- package/src/features/email-automation/data/EmailAutomation.ts +87 -0
- package/src/features/email-automation/data/EmailAutomationEnrollment.ts +105 -0
- package/src/features/email-automation/data/EmailAutomationEnrollmentInterface.ts +18 -0
- package/src/features/email-automation/data/EmailAutomationEnrollmentService.ts +31 -0
- package/src/features/email-automation/data/EmailAutomationInterface.ts +22 -0
- package/src/features/email-automation/data/EmailAutomationService.ts +106 -0
- package/src/features/email-automation/data/EmailAutomationStep.ts +104 -0
- package/src/features/email-automation/data/EmailAutomationStepInterface.ts +23 -0
- package/src/features/email-automation/data/EmailAutomationStepService.ts +80 -0
- package/src/features/email-automation/data/EmailAutomationTrigger.ts +28 -0
- package/src/features/email-automation/data/EmailAutomationTriggerInterface.ts +6 -0
- package/src/features/email-automation/data/__tests__/email-automation-models.spec.ts +103 -0
- package/src/features/email-automation/data/email-automation-enrollment.fields.ts +8 -0
- package/src/features/email-automation/data/email-automation.fields.ts +9 -0
- package/src/features/email-automation/data/index.ts +13 -0
- package/src/features/email-automation/hooks/index.ts +2 -0
- package/src/features/email-automation/hooks/useEmailAutomationEnrollmentTableStructure.tsx +126 -0
- package/src/features/email-automation/hooks/useEmailAutomationTableStructure.tsx +119 -0
- package/src/features/email-automation/index.ts +5 -0
- package/src/features/s3/data/s3.service.ts +17 -0
- package/dist/BlockNoteEditor-2QJVK2HH.mjs.map +0 -1
- package/dist/BlockNoteEditor-EGY4ZAAM.js.map +0 -1
- package/dist/chunk-2Z3ZCYXX.mjs.map +0 -1
- package/dist/chunk-HNOWSFM5.mjs.map +0 -1
- package/dist/chunk-S5MQDWWC.js.map +0 -1
- package/dist/chunk-THKYH65N.js.map +0 -1
- package/dist/chunk-YMUPTXOT.mjs.map +0 -1
- /package/dist/{BlockNoteViewer-6BQXJ4TB.mjs.map → BlockNoteViewer-AEJKJ27Q.mjs.map} +0 -0
- /package/dist/{chunk-NPKFMRH5.mjs.map → chunk-6DC2WVVB.mjs.map} +0 -0
- /package/dist/{chunk-JEZQTZQ4.mjs.map → chunk-QMYQRPQC.mjs.map} +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { useMemo } from "react";
|
|
5
|
+
import { RoundPageContainer } from "../../../../components";
|
|
6
|
+
import { SharedProvider } from "../../../../contexts/SharedContext";
|
|
7
|
+
import { Modules } from "../../../../core";
|
|
8
|
+
import { usePageUrlGenerator } from "../../../../hooks";
|
|
9
|
+
import { BreadcrumbItemData } from "../../../../interfaces";
|
|
10
|
+
import { Badge } from "../../../../shadcnui";
|
|
11
|
+
import { BlogPostProvider, useBlogPost } from "../../contexts/BlogPostContext";
|
|
12
|
+
import { BlogPostDeleter } from "./BlogPostDeleter";
|
|
13
|
+
import { BlogPostPublishButton } from "./BlogPostPublishButton";
|
|
14
|
+
import { BlogPostTranslationPanel } from "./BlogPostTranslationPanel";
|
|
15
|
+
import { BLOG_STATUS_BADGE, useBlogLanguageName } from "./BlogTranslationStatus";
|
|
16
|
+
|
|
17
|
+
export type BlogPostContainerProps = {
|
|
18
|
+
postId: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Admin post page: one tab per language, the original first and marked
|
|
23
|
+
* "Original" (approved mockup A). Mirrors
|
|
24
|
+
* features/email-automation/components/containers/EmailAutomationContainer.tsx.
|
|
25
|
+
*/
|
|
26
|
+
export function BlogPostContainer({ postId }: BlogPostContainerProps) {
|
|
27
|
+
return (
|
|
28
|
+
<BlogPostProvider postId={postId}>
|
|
29
|
+
<BlogPostShell />
|
|
30
|
+
</BlogPostProvider>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function BlogPostShell() {
|
|
35
|
+
const t = useTranslations();
|
|
36
|
+
const generateUrl = usePageUrlGenerator();
|
|
37
|
+
const languageName = useBlogLanguageName();
|
|
38
|
+
const { post, reload } = useBlogPost();
|
|
39
|
+
|
|
40
|
+
const translations = useMemo(() => {
|
|
41
|
+
const all = post?.translations ?? [];
|
|
42
|
+
return [
|
|
43
|
+
...all.filter((translation) => translation.isOriginal),
|
|
44
|
+
...all.filter((translation) => !translation.isOriginal),
|
|
45
|
+
];
|
|
46
|
+
}, [post]);
|
|
47
|
+
|
|
48
|
+
if (!post) return null;
|
|
49
|
+
|
|
50
|
+
const title = post.original?.title ?? post.title ?? "";
|
|
51
|
+
|
|
52
|
+
const breadcrumbs: BreadcrumbItemData[] = [
|
|
53
|
+
{ name: t("blog.admin.title"), href: generateUrl({ page: Modules.BlogPost }) },
|
|
54
|
+
{ name: title },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const statusLabel = {
|
|
58
|
+
draft: t("blog.status.draft"),
|
|
59
|
+
published: t("blog.status.published"),
|
|
60
|
+
unpublished: t("blog.status.unpublished"),
|
|
61
|
+
}[post.status];
|
|
62
|
+
|
|
63
|
+
const tabLabel = (translation: (typeof translations)[number]) => {
|
|
64
|
+
const parts = [languageName(translation.language)];
|
|
65
|
+
if (translation.isOriginal) parts.push(t("blog.translation.original"));
|
|
66
|
+
if (translation.aiStatus === "pending" || translation.aiStatus === "in_progress")
|
|
67
|
+
parts.push(t("blog.translation.translating"));
|
|
68
|
+
if (translation.aiStatus === "failed") parts.push(t("blog.translation.failed"));
|
|
69
|
+
return parts.join(" · ");
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<SharedProvider
|
|
74
|
+
value={{
|
|
75
|
+
breadcrumbs,
|
|
76
|
+
title: {
|
|
77
|
+
type: t("blog.admin.title"),
|
|
78
|
+
element: title,
|
|
79
|
+
functions: [
|
|
80
|
+
<Badge key="blogPostStatus" variant={BLOG_STATUS_BADGE[post.status]}>
|
|
81
|
+
{statusLabel}
|
|
82
|
+
</Badge>,
|
|
83
|
+
<BlogPostPublishButton key="blogPostPublish" />,
|
|
84
|
+
<BlogPostDeleter key="blogPostDeleter" />,
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
<RoundPageContainer
|
|
90
|
+
module={Modules.BlogPost}
|
|
91
|
+
id={post.id}
|
|
92
|
+
fullWidth
|
|
93
|
+
forceHeader
|
|
94
|
+
tabs={translations.map((translation) => ({
|
|
95
|
+
sectionKey: translation.language,
|
|
96
|
+
label: tabLabel(translation),
|
|
97
|
+
content: <BlogPostTranslationPanel post={post} translation={translation} onChanged={reload} />,
|
|
98
|
+
}))}
|
|
99
|
+
/>
|
|
100
|
+
</SharedProvider>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { CommonDeleter } from "../../../../components";
|
|
5
|
+
import { Modules } from "../../../../core";
|
|
6
|
+
import { usePageUrlGenerator } from "../../../../hooks";
|
|
7
|
+
import { useBlogPost } from "../../contexts/BlogPostContext";
|
|
8
|
+
import { BlogPostService } from "../../data";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Deletes the post and returns to the blog admin page.
|
|
12
|
+
* Mirrors features/how-to/components/forms/HowToDeleter.tsx. The dialog texts
|
|
13
|
+
* are passed explicitly: CommonDeleter's default reads `entities.<type>`, a
|
|
14
|
+
* namespace only the app may fill, so the blog's own entity name is used.
|
|
15
|
+
*/
|
|
16
|
+
export function BlogPostDeleter() {
|
|
17
|
+
const t = useTranslations();
|
|
18
|
+
const generateUrl = usePageUrlGenerator();
|
|
19
|
+
const { post } = useBlogPost();
|
|
20
|
+
|
|
21
|
+
if (!post) return null;
|
|
22
|
+
|
|
23
|
+
const type = t("blog.entities.post");
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<CommonDeleter
|
|
27
|
+
title={t("common.delete.title", { type })}
|
|
28
|
+
subtitle={t("common.delete.subtitle", { type })}
|
|
29
|
+
description={<div className="text-destructive p-4 text-sm">{t("common.delete.description", { type })}</div>}
|
|
30
|
+
showLabel
|
|
31
|
+
deleteFunction={() => BlogPostService.delete({ id: post.id })}
|
|
32
|
+
redirectTo={generateUrl({ page: Modules.BlogPost })}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { ReactNode, useCallback, useMemo, useState } from "react";
|
|
6
|
+
import { useForm } from "react-hook-form";
|
|
7
|
+
import { v4 } from "uuid";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import {
|
|
10
|
+
EditorSheet,
|
|
11
|
+
EntityMultiSelector,
|
|
12
|
+
FormBlockNote,
|
|
13
|
+
FormInput,
|
|
14
|
+
FormSelect,
|
|
15
|
+
FormTextarea,
|
|
16
|
+
} from "../../../../components";
|
|
17
|
+
import { Modules } from "../../../../core";
|
|
18
|
+
import { getI18nSupportedLocales, useI18nLocale, useI18nRouter } from "../../../../i18n";
|
|
19
|
+
import {
|
|
20
|
+
BlogCategoryInterface,
|
|
21
|
+
BlogCategoryService,
|
|
22
|
+
BlogPostInput,
|
|
23
|
+
BlogPostInterface,
|
|
24
|
+
BlogPostService,
|
|
25
|
+
} from "../../data";
|
|
26
|
+
import { BlogCoverImageField, BlogImageSelection, uploadBlogImage } from "./BlogCoverImageField";
|
|
27
|
+
import { blogCategoryAdminName, useBlogLanguageName } from "./BlogTranslationStatus";
|
|
28
|
+
import { BLOG_POST_UPLOAD_PREFIX } from "../../types/blog.constants";
|
|
29
|
+
import { useBlogCategoryNames } from "../../hooks/useBlogCategoryNames";
|
|
30
|
+
|
|
31
|
+
type BlogPostEditorProps = {
|
|
32
|
+
/** Edit mode when present: the post's original translation and shared fields. */
|
|
33
|
+
post?: BlogPostInterface;
|
|
34
|
+
trigger?: ReactNode;
|
|
35
|
+
onSuccess?: () => void | Promise<void>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const isBodyEmpty = (body: any) => !body || (Array.isArray(body) && body.length === 0);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates a post, or edits its original-language text plus the fields every
|
|
42
|
+
* language shares (cover image, categories). Saving the original makes the
|
|
43
|
+
* worker translate it again. Mirrors features/how-to/components/forms/HowToEditor.tsx.
|
|
44
|
+
*/
|
|
45
|
+
export function BlogPostEditor({ post, trigger, onSuccess }: BlogPostEditorProps) {
|
|
46
|
+
const t = useTranslations();
|
|
47
|
+
const categoryName = useBlogCategoryNames();
|
|
48
|
+
const router = useI18nRouter();
|
|
49
|
+
const locale = useI18nLocale();
|
|
50
|
+
const languageName = useBlogLanguageName();
|
|
51
|
+
const original = post?.original;
|
|
52
|
+
const [cover, setCover] = useState<BlogImageSelection>(null);
|
|
53
|
+
const [isBodyEmptyState, setIsBodyEmptyState] = useState<boolean>(isBodyEmpty(original?.body));
|
|
54
|
+
|
|
55
|
+
const languages = useMemo(() => getI18nSupportedLocales(), []);
|
|
56
|
+
|
|
57
|
+
const formSchema = useMemo(
|
|
58
|
+
() =>
|
|
59
|
+
z.object({
|
|
60
|
+
id: z.string().uuid(),
|
|
61
|
+
originalLanguage: z.string().min(1, { message: t("blog.errors.language") }),
|
|
62
|
+
title: z.string().min(1, { message: t("blog.errors.title") }),
|
|
63
|
+
slug: z.string().optional(),
|
|
64
|
+
summary: z.string().optional(),
|
|
65
|
+
body: z.any(),
|
|
66
|
+
coverImage: z.string().optional(),
|
|
67
|
+
coverImageAlt: z.string().optional(),
|
|
68
|
+
categories: z.array(z.object({ id: z.string(), name: z.string() })),
|
|
69
|
+
}),
|
|
70
|
+
[],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const getDefaultValues = useCallback(
|
|
74
|
+
() => ({
|
|
75
|
+
id: post?.id || v4(),
|
|
76
|
+
originalLanguage: post?.originalLanguage ?? (languages.includes(locale) ? locale : languages[0]),
|
|
77
|
+
title: original?.title ?? "",
|
|
78
|
+
slug: original?.slug ?? "",
|
|
79
|
+
summary: original?.summary ?? "",
|
|
80
|
+
body: original?.body || [],
|
|
81
|
+
// The stored value is a signed URL, not a key: only a newly picked file
|
|
82
|
+
// writes a key here, and only then is the field sent.
|
|
83
|
+
coverImage: "",
|
|
84
|
+
coverImageAlt: original?.coverImageAlt ?? "",
|
|
85
|
+
categories: (post?.categories ?? []).map((category) => ({
|
|
86
|
+
id: category.id,
|
|
87
|
+
name: blogCategoryAdminName(category),
|
|
88
|
+
})),
|
|
89
|
+
}),
|
|
90
|
+
[post, original, languages, locale],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const form = useForm<z.infer<typeof formSchema>>({
|
|
94
|
+
resolver: zodResolver(formSchema),
|
|
95
|
+
defaultValues: getDefaultValues(),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const { dirtyFields } = form.formState;
|
|
99
|
+
const isFormDirty = useCallback(() => {
|
|
100
|
+
const dirty: Record<string, unknown> = { ...dirtyFields };
|
|
101
|
+
if (dirty.body && isBodyEmptyState) delete dirty.body;
|
|
102
|
+
return Object.keys(dirty).length > 0;
|
|
103
|
+
}, [dirtyFields, isBodyEmptyState]);
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<EditorSheet
|
|
107
|
+
form={form}
|
|
108
|
+
entityType={t("blog.entities.post")}
|
|
109
|
+
entityName={original?.title}
|
|
110
|
+
title={post ? t("blog.admin.editPost") : t("blog.admin.newPost")}
|
|
111
|
+
isEdit={!!post}
|
|
112
|
+
module={Modules.BlogPost}
|
|
113
|
+
size="lg"
|
|
114
|
+
isFormDirty={isFormDirty}
|
|
115
|
+
onSubmit={async (values) => {
|
|
116
|
+
if (values.coverImage && cover) await uploadBlogImage({ key: values.coverImage, selection: cover });
|
|
117
|
+
|
|
118
|
+
const payload: BlogPostInput = {
|
|
119
|
+
id: values.id,
|
|
120
|
+
title: values.title,
|
|
121
|
+
// An empty summary lets the worker write one; on edit the summary is
|
|
122
|
+
// sent only when changed here, so an untouched AI summary is regenerated.
|
|
123
|
+
summary: post
|
|
124
|
+
? form.getFieldState("summary").isDirty
|
|
125
|
+
? values.summary
|
|
126
|
+
: undefined
|
|
127
|
+
: values.summary || undefined,
|
|
128
|
+
body: values.body,
|
|
129
|
+
coverImage: values.coverImage && cover ? values.coverImage : undefined,
|
|
130
|
+
coverImageAlt: values.coverImageAlt,
|
|
131
|
+
categoryIds: values.categories.map((category) => category.id),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
if (post) {
|
|
135
|
+
return BlogPostService.update({
|
|
136
|
+
...payload,
|
|
137
|
+
originalLanguage: post.originalLanguage,
|
|
138
|
+
slug: values.slug || undefined,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return BlogPostService.create({ ...payload, originalLanguage: values.originalLanguage });
|
|
142
|
+
}}
|
|
143
|
+
onReset={() => {
|
|
144
|
+
setCover(null);
|
|
145
|
+
setIsBodyEmptyState(isBodyEmpty(original?.body));
|
|
146
|
+
return getDefaultValues();
|
|
147
|
+
}}
|
|
148
|
+
onSuccess={onSuccess}
|
|
149
|
+
onNavigate={(url) => router.push(url)}
|
|
150
|
+
trigger={trigger}
|
|
151
|
+
>
|
|
152
|
+
<div className="flex w-full flex-col gap-y-4">
|
|
153
|
+
{!post && (
|
|
154
|
+
<FormSelect
|
|
155
|
+
form={form}
|
|
156
|
+
id="originalLanguage"
|
|
157
|
+
name={t("blog.fields.originalLanguage")}
|
|
158
|
+
values={languages.map((language) => ({ id: language, text: languageName(language) }))}
|
|
159
|
+
isRequired
|
|
160
|
+
/>
|
|
161
|
+
)}
|
|
162
|
+
<FormInput form={form} id="title" name={t("blog.fields.title")} isRequired />
|
|
163
|
+
{post && (
|
|
164
|
+
<FormInput form={form} id="slug" name={t("blog.fields.slug")} description={t("blog.fields.slugHelp")} />
|
|
165
|
+
)}
|
|
166
|
+
<FormTextarea
|
|
167
|
+
form={form}
|
|
168
|
+
id="summary"
|
|
169
|
+
name={t("blog.fields.summary")}
|
|
170
|
+
description={t("blog.fields.summaryHelp")}
|
|
171
|
+
/>
|
|
172
|
+
<BlogCoverImageField
|
|
173
|
+
form={form}
|
|
174
|
+
id="coverImage"
|
|
175
|
+
label={t("blog.fields.coverImage")}
|
|
176
|
+
keyPrefix={BLOG_POST_UPLOAD_PREFIX}
|
|
177
|
+
entityId={form.getValues("id")}
|
|
178
|
+
currentUrl={post?.coverImage}
|
|
179
|
+
alt={original?.coverImageAlt}
|
|
180
|
+
selection={cover}
|
|
181
|
+
onSelectionChange={setCover}
|
|
182
|
+
/>
|
|
183
|
+
<FormInput form={form} id="coverImageAlt" name={t("blog.fields.coverImageAlt")} />
|
|
184
|
+
<EntityMultiSelector<BlogCategoryInterface>
|
|
185
|
+
id="categories"
|
|
186
|
+
form={form}
|
|
187
|
+
label={t("blog.fields.categories")}
|
|
188
|
+
retriever={(params) => BlogCategoryService.findMany(params)}
|
|
189
|
+
module={Modules.BlogCategory}
|
|
190
|
+
getLabel={blogCategoryAdminName}
|
|
191
|
+
// A post's included categories carry no translations, so the stored chip
|
|
192
|
+
// names come from the category list.
|
|
193
|
+
getFormValueLabel={(value: { id: string; name?: string }) => value.name || categoryName(value.id)}
|
|
194
|
+
/>
|
|
195
|
+
<FormBlockNote
|
|
196
|
+
uploadKeyPrefix={BLOG_POST_UPLOAD_PREFIX}
|
|
197
|
+
form={form}
|
|
198
|
+
id="body"
|
|
199
|
+
name={t("blog.fields.body")}
|
|
200
|
+
type="blog-post"
|
|
201
|
+
isRequired
|
|
202
|
+
onEmptyChange={setIsBodyEmptyState}
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
</EditorSheet>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { errorToast } from "../../../../components";
|
|
7
|
+
import { Button } from "../../../../shadcnui";
|
|
8
|
+
import { useBlogPost } from "../../contexts/BlogPostContext";
|
|
9
|
+
import { BlogPostService } from "../../data";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Publish / Unpublish in the post page header. Publishing an unpublished post
|
|
13
|
+
* republishes it (the first publish date is kept server-side).
|
|
14
|
+
* Mirrors features/email-automation/components/canvas/EmailAutomationActiveSwitch.tsx.
|
|
15
|
+
*/
|
|
16
|
+
export function BlogPostPublishButton() {
|
|
17
|
+
const t = useTranslations();
|
|
18
|
+
const { post, reload } = useBlogPost();
|
|
19
|
+
const [saving, setSaving] = useState<boolean>(false);
|
|
20
|
+
|
|
21
|
+
if (!post) return null;
|
|
22
|
+
|
|
23
|
+
const isPublished = post.status === "published";
|
|
24
|
+
|
|
25
|
+
const toggle = async () => {
|
|
26
|
+
setSaving(true);
|
|
27
|
+
try {
|
|
28
|
+
if (isPublished) await BlogPostService.unpublish({ id: post.id });
|
|
29
|
+
else await BlogPostService.publish({ id: post.id });
|
|
30
|
+
await reload();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
errorToast({ title: t("common.errors.error"), error });
|
|
33
|
+
} finally {
|
|
34
|
+
setSaving(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Button
|
|
40
|
+
type="button"
|
|
41
|
+
size="sm"
|
|
42
|
+
variant={isPublished ? "outline" : "default"}
|
|
43
|
+
onClick={() => void toggle()}
|
|
44
|
+
disabled={saving}
|
|
45
|
+
>
|
|
46
|
+
{isPublished ? <EyeOffIcon /> : <EyeIcon />}
|
|
47
|
+
{isPublished ? t("blog.actions.unpublish") : t("blog.actions.publish")}
|
|
48
|
+
</Button>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { ReactNode, useCallback, useMemo, useState } from "react";
|
|
6
|
+
import { useForm } from "react-hook-form";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { EditorSheet, FormBlockNote, FormInput, FormTextarea } from "../../../../components";
|
|
9
|
+
import { Modules } from "../../../../core";
|
|
10
|
+
import { BlogPostTranslationInterface, BlogPostTranslationService } from "../../data";
|
|
11
|
+
import { useBlogLanguageName } from "./BlogTranslationStatus";
|
|
12
|
+
import { BLOG_POST_UPLOAD_PREFIX } from "../../types/blog.constants";
|
|
13
|
+
|
|
14
|
+
type BlogPostTranslationEditorProps = {
|
|
15
|
+
translation: BlogPostTranslationInterface;
|
|
16
|
+
trigger?: ReactNode;
|
|
17
|
+
onSuccess?: () => void | Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const isBodyEmpty = (body: any) => !body || (Array.isArray(body) && body.length === 0);
|
|
21
|
+
|
|
22
|
+
const buildFormSchema = (t: (key: string) => string) =>
|
|
23
|
+
z.object({
|
|
24
|
+
id: z.string().uuid(),
|
|
25
|
+
title: z.string().min(1, { message: t("blog.errors.title") }),
|
|
26
|
+
slug: z.string().optional(),
|
|
27
|
+
summary: z.string().optional(),
|
|
28
|
+
body: z.any(),
|
|
29
|
+
coverImageAlt: z.string().optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Edits one non-original translation by hand. Saving marks it `manual` and
|
|
34
|
+
* re-runs nothing. Mirrors features/how-to/components/forms/HowToEditor.tsx.
|
|
35
|
+
*/
|
|
36
|
+
export function BlogPostTranslationEditor({ translation, trigger, onSuccess }: BlogPostTranslationEditorProps) {
|
|
37
|
+
const t = useTranslations();
|
|
38
|
+
const formSchema = useMemo(() => buildFormSchema(t), [t]);
|
|
39
|
+
const languageName = useBlogLanguageName();
|
|
40
|
+
const [isBodyEmptyState, setIsBodyEmptyState] = useState<boolean>(isBodyEmpty(translation.body));
|
|
41
|
+
|
|
42
|
+
const getDefaultValues = useCallback(
|
|
43
|
+
() => ({
|
|
44
|
+
id: translation.id,
|
|
45
|
+
title: translation.title ?? "",
|
|
46
|
+
slug: translation.slug ?? "",
|
|
47
|
+
summary: translation.summary ?? "",
|
|
48
|
+
body: translation.body || [],
|
|
49
|
+
coverImageAlt: translation.coverImageAlt ?? "",
|
|
50
|
+
}),
|
|
51
|
+
[translation],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const form = useForm<z.infer<typeof formSchema>>({
|
|
55
|
+
resolver: zodResolver(formSchema),
|
|
56
|
+
defaultValues: getDefaultValues(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const { dirtyFields } = form.formState;
|
|
60
|
+
const isFormDirty = useCallback(() => {
|
|
61
|
+
const dirty: Record<string, unknown> = { ...dirtyFields };
|
|
62
|
+
if (dirty.body && isBodyEmptyState) delete dirty.body;
|
|
63
|
+
return Object.keys(dirty).length > 0;
|
|
64
|
+
}, [dirtyFields, isBodyEmptyState]);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<EditorSheet
|
|
68
|
+
form={form}
|
|
69
|
+
entityType={t("blog.entities.post")}
|
|
70
|
+
entityName={translation.title}
|
|
71
|
+
title={`${t("blog.admin.editPost")} · ${languageName(translation.language)}`}
|
|
72
|
+
isEdit
|
|
73
|
+
module={Modules.BlogPostTranslation}
|
|
74
|
+
size="lg"
|
|
75
|
+
isFormDirty={isFormDirty}
|
|
76
|
+
onSubmit={async (values) =>
|
|
77
|
+
BlogPostTranslationService.update({
|
|
78
|
+
id: values.id,
|
|
79
|
+
title: values.title,
|
|
80
|
+
slug: values.slug || undefined,
|
|
81
|
+
summary: values.summary,
|
|
82
|
+
body: values.body,
|
|
83
|
+
coverImageAlt: values.coverImageAlt,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
onReset={() => {
|
|
87
|
+
setIsBodyEmptyState(isBodyEmpty(translation.body));
|
|
88
|
+
return getDefaultValues();
|
|
89
|
+
}}
|
|
90
|
+
onSuccess={onSuccess ?? (() => {})}
|
|
91
|
+
trigger={trigger}
|
|
92
|
+
>
|
|
93
|
+
<div className="flex w-full flex-col gap-y-4">
|
|
94
|
+
<FormInput form={form} id="title" name={t("blog.fields.title")} isRequired />
|
|
95
|
+
<FormInput form={form} id="slug" name={t("blog.fields.slug")} description={t("blog.fields.slugHelp")} />
|
|
96
|
+
<FormTextarea form={form} id="summary" name={t("blog.fields.summary")} />
|
|
97
|
+
<FormInput form={form} id="coverImageAlt" name={t("blog.fields.coverImageAlt")} />
|
|
98
|
+
<FormBlockNote
|
|
99
|
+
uploadKeyPrefix={BLOG_POST_UPLOAD_PREFIX}
|
|
100
|
+
form={form}
|
|
101
|
+
id="body"
|
|
102
|
+
name={t("blog.fields.body")}
|
|
103
|
+
type="blog-post"
|
|
104
|
+
isRequired
|
|
105
|
+
onEmptyChange={setIsBodyEmptyState}
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
</EditorSheet>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { PencilIcon } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import Image from "next/image";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { DetailField } from "../../../../components";
|
|
8
|
+
import { Alert, AlertDescription, Badge, Button } from "../../../../shadcnui";
|
|
9
|
+
import { isRemoteImageSrc } from "../../../../utils";
|
|
10
|
+
import { BlogPostInterface, BlogPostTranslationInterface, BlogPostTranslationService } from "../../data";
|
|
11
|
+
import { BlogPostComparison, BlogPostTranslationFields } from "./BlogPostComparison";
|
|
12
|
+
import { BlogPostEditor } from "./BlogPostEditor";
|
|
13
|
+
import { BlogPostTranslationEditor } from "./BlogPostTranslationEditor";
|
|
14
|
+
import { useBlogCategoryNames } from "../../hooks/useBlogCategoryNames";
|
|
15
|
+
import { BlogTranslationStatus, useBlogDateFormatter, useBlogLanguageName } from "./BlogTranslationStatus";
|
|
16
|
+
|
|
17
|
+
type BlogPostTranslationPanelProps = {
|
|
18
|
+
post: BlogPostInterface;
|
|
19
|
+
translation: BlogPostTranslationInterface;
|
|
20
|
+
/** Re-reads the post after an edit or a re-translation request. */
|
|
21
|
+
onChanged?: () => Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* One language tab of the admin post page (approved mockup A).
|
|
26
|
+
* - Original tab: cover image, categories, status, the text fields and an Edit
|
|
27
|
+
* that opens BlogPostEditor (which re-translates every other language).
|
|
28
|
+
* - Translation tab: where the text came from, whether it was edited by hand,
|
|
29
|
+
* the overwrite warning, "Compare with original", the job status, the text
|
|
30
|
+
* fields and an Edit that opens BlogPostTranslationEditor.
|
|
31
|
+
*/
|
|
32
|
+
export function BlogPostTranslationPanel({ post, translation, onChanged }: BlogPostTranslationPanelProps) {
|
|
33
|
+
const t = useTranslations();
|
|
34
|
+
const categoryName = useBlogCategoryNames();
|
|
35
|
+
const languageName = useBlogLanguageName();
|
|
36
|
+
const formatDate = useBlogDateFormatter();
|
|
37
|
+
const [isComparing, setIsComparing] = useState<boolean>(false);
|
|
38
|
+
|
|
39
|
+
const original = post.original;
|
|
40
|
+
const reload = onChanged ?? (async () => {});
|
|
41
|
+
|
|
42
|
+
const editTrigger = (
|
|
43
|
+
<Button render={<div />} nativeButton={false} size="sm" variant="outline">
|
|
44
|
+
<PencilIcon />
|
|
45
|
+
{t("blog.actions.edit")}
|
|
46
|
+
</Button>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (translation.isOriginal) {
|
|
50
|
+
const statusLabel = {
|
|
51
|
+
draft: t("blog.status.draft"),
|
|
52
|
+
published: t("blog.status.published"),
|
|
53
|
+
unpublished: t("blog.status.unpublished"),
|
|
54
|
+
}[post.status];
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="flex w-full flex-col gap-y-6 p-4">
|
|
58
|
+
<div className="flex items-center justify-between gap-x-4">
|
|
59
|
+
<Badge variant="softGray">{t("blog.translation.original")}</Badge>
|
|
60
|
+
<BlogPostEditor post={post} trigger={editTrigger} onSuccess={reload} />
|
|
61
|
+
</div>
|
|
62
|
+
{post.coverImage && (
|
|
63
|
+
<Image
|
|
64
|
+
src={post.coverImage}
|
|
65
|
+
alt={translation.coverImageAlt ?? translation.title}
|
|
66
|
+
width={640}
|
|
67
|
+
height={360}
|
|
68
|
+
className="h-auto max-h-72 w-auto rounded-lg object-cover"
|
|
69
|
+
unoptimized={isRemoteImageSrc(post.coverImage)}
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
73
|
+
<DetailField label={t("blog.fields.status")} value={statusLabel} />
|
|
74
|
+
<DetailField label={t("blog.fields.publishedAt")} value={formatDate(post.publishedAt) || "—"} />
|
|
75
|
+
<DetailField
|
|
76
|
+
label={t("blog.fields.categories")}
|
|
77
|
+
value={
|
|
78
|
+
post.categories
|
|
79
|
+
.map((category) => categoryName(category.id))
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.join(", ") || "—"
|
|
82
|
+
}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
<BlogPostTranslationFields translation={translation} />
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const isWorking = translation.aiStatus === "pending" || translation.aiStatus === "in_progress";
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className="flex w-full flex-col gap-y-6 p-4">
|
|
94
|
+
<Alert>
|
|
95
|
+
<AlertDescription className="flex flex-col gap-y-2">
|
|
96
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
97
|
+
{translation.source === "manual" && <Badge variant="softAmber">{t("blog.translation.editedByHand")}</Badge>}
|
|
98
|
+
{original && translation.translatedAt && (
|
|
99
|
+
<span>
|
|
100
|
+
{t("blog.translation.translatedFrom", {
|
|
101
|
+
language: languageName(original.language),
|
|
102
|
+
date: formatDate(translation.translatedAt),
|
|
103
|
+
})}
|
|
104
|
+
</span>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
<span>{t("blog.translation.overwriteWarning")}</span>
|
|
108
|
+
{original && (
|
|
109
|
+
<div>
|
|
110
|
+
<Button type="button" size="sm" variant="ghost" onClick={() => setIsComparing((value) => !value)}>
|
|
111
|
+
{isComparing ? t("blog.actions.hideComparison") : t("blog.actions.compareWithOriginal")}
|
|
112
|
+
</Button>
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</AlertDescription>
|
|
116
|
+
</Alert>
|
|
117
|
+
|
|
118
|
+
<div className="flex items-center justify-between gap-x-4">
|
|
119
|
+
<BlogTranslationStatus
|
|
120
|
+
aiStatus={translation.aiStatus}
|
|
121
|
+
onRetranslate={async () => {
|
|
122
|
+
await BlogPostTranslationService.retranslate({ id: translation.id });
|
|
123
|
+
await reload();
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
{!isWorking && <BlogPostTranslationEditor translation={translation} trigger={editTrigger} onSuccess={reload} />}
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
{isComparing && original ? (
|
|
130
|
+
<BlogPostComparison original={original} translation={translation} />
|
|
131
|
+
) : (
|
|
132
|
+
<BlogPostTranslationFields translation={translation} />
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|