create-nextblock 0.12.16 → 0.13.2
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/package.json +1 -1
- package/templates/nextblock-template/app/actions/interactions.ts +27 -4
- package/templates/nextblock-template/app/api/ai/global-agent/route.ts +287 -48
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +238 -209
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +103 -8
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +31 -2
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +37 -15
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +26 -15
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +123 -46
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +8 -1
- package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +62 -22
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +40 -2
- package/templates/nextblock-template/app/cms/interactions/EmailRecipientsInput.tsx +189 -0
- package/templates/nextblock-template/app/cms/interactions/InteractionsModerationClient.tsx +138 -71
- package/templates/nextblock-template/app/cms/media/import-external-image.ts +289 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +13 -10
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +14 -3
- package/templates/nextblock-template/app/cms/pages/actions.ts +59 -6
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +21 -11
- package/templates/nextblock-template/app/cms/posts/actions.ts +45 -0
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +11 -9
- package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +463 -227
- package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +220 -1
- package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +11 -0
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +20 -1
- package/templates/nextblock-template/app/cms/users/actions.ts +69 -0
- package/templates/nextblock-template/app/cms/users/components/CreateUserForm.tsx +217 -0
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +4 -1
- package/templates/nextblock-template/app/cms/users/new/page.tsx +44 -0
- package/templates/nextblock-template/app/cms/users/page.tsx +12 -3
- package/templates/nextblock-template/app/lib/homepage.ts +36 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +13 -6
- package/templates/nextblock-template/app/page.tsx +55 -12
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +56 -0
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +60 -30
- package/templates/nextblock-template/components/blocks/renderers/StockPhotoCredit.tsx +167 -0
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +94 -6
- package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +7 -1
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +29 -3
- package/templates/nextblock-template/lib/search/server.ts +11 -1
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +82 -72
- package/templates/nextblock-template/next-env.d.ts +1 -1
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/proxy.ts +5 -0
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
|
@@ -7,6 +7,7 @@ import { redirect } from "next/navigation";
|
|
|
7
7
|
import type { Database } from "@nextblock-cms/db";
|
|
8
8
|
import { v4 as uuidv4 } from 'uuid';
|
|
9
9
|
import { getOrCreateContentDraft } from "../../../lib/visual-editing/draft-content";
|
|
10
|
+
import { getHomepageTranslationGroupId } from "../../lib/homepage";
|
|
10
11
|
|
|
11
12
|
type PageStatus = Database['public']['Enums']['page_status'];
|
|
12
13
|
import { encodedRedirect } from "@nextblock-cms/utils/server";
|
|
@@ -18,11 +19,16 @@ function getOptionalFeatureImageId(formData: FormData) {
|
|
|
18
19
|
return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
function revalidatePublicPageSlug(
|
|
22
|
+
function revalidatePublicPageSlug(
|
|
23
|
+
slug: string | null | undefined,
|
|
24
|
+
isHomepage = false
|
|
25
|
+
) {
|
|
22
26
|
if (!slug) return;
|
|
23
27
|
|
|
24
28
|
revalidatePath(`/${slug}`);
|
|
25
|
-
|
|
29
|
+
// Any language variation of the homepage is also served at "/", regardless of
|
|
30
|
+
// its slug — bust that cache too (the literal-slug check is a cheap fallback).
|
|
31
|
+
if (isHomepage || slug === "home" || slug === "accueil") {
|
|
26
32
|
revalidatePath("/");
|
|
27
33
|
}
|
|
28
34
|
}
|
|
@@ -91,7 +97,11 @@ export async function createPage(formData: FormData) {
|
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
revalidatePath("/cms/pages");
|
|
94
|
-
|
|
100
|
+
const createHomepageGroupId = await getHomepageTranslationGroupId(supabase);
|
|
101
|
+
revalidatePublicPageSlug(
|
|
102
|
+
newPage?.slug,
|
|
103
|
+
!!createHomepageGroupId && newPage?.translation_group_id === createHomepageGroupId
|
|
104
|
+
);
|
|
95
105
|
|
|
96
106
|
if (newPage?.id) {
|
|
97
107
|
redirect(`/cms/pages/${newPage.id}/edit?success=${encodeURIComponent("Page created successfully.")}`);
|
|
@@ -168,9 +178,12 @@ export async function updatePage(pageId: number, formData: FormData) {
|
|
|
168
178
|
}
|
|
169
179
|
|
|
170
180
|
revalidatePath("/cms/pages");
|
|
171
|
-
|
|
181
|
+
const updateHomepageGroupId = await getHomepageTranslationGroupId(supabase);
|
|
182
|
+
const updateIsHomepage =
|
|
183
|
+
!!updateHomepageGroupId && existingPage.translation_group_id === updateHomepageGroupId;
|
|
184
|
+
revalidatePublicPageSlug(existingPage.slug, updateIsHomepage);
|
|
172
185
|
if (rawFormData.slug && rawFormData.slug !== existingPage.slug) {
|
|
173
|
-
revalidatePublicPageSlug(rawFormData.slug);
|
|
186
|
+
revalidatePublicPageSlug(rawFormData.slug, updateIsHomepage);
|
|
174
187
|
}
|
|
175
188
|
|
|
176
189
|
revalidatePath(pageEditPath);
|
|
@@ -178,6 +191,39 @@ export async function updatePage(pageId: number, formData: FormData) {
|
|
|
178
191
|
}
|
|
179
192
|
|
|
180
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Publish a page directly (status -> "published") so it becomes visible on the
|
|
196
|
+
* live site. Used by the draft-aware "View Live" button when an admin chooses to
|
|
197
|
+
* publish a still-draft page. Revalidates the public surfaces, including "/" when
|
|
198
|
+
* the page belongs to the homepage translation group.
|
|
199
|
+
*/
|
|
200
|
+
export async function publishPage(pageId: number): Promise<{ error?: string } | void> {
|
|
201
|
+
const supabase = createClient();
|
|
202
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
203
|
+
if (!user) return { error: "User not authenticated." };
|
|
204
|
+
|
|
205
|
+
const { data: page, error } = await supabase
|
|
206
|
+
.from("pages")
|
|
207
|
+
.update({ status: "published", updated_at: new Date().toISOString() })
|
|
208
|
+
.eq("id", pageId)
|
|
209
|
+
.select("slug, translation_group_id")
|
|
210
|
+
.single();
|
|
211
|
+
|
|
212
|
+
if (error || !page) {
|
|
213
|
+
return { error: error?.message || "Could not publish the page." };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
revalidatePath("/cms/pages");
|
|
217
|
+
revalidatePath(`/cms/pages/${pageId}/edit`);
|
|
218
|
+
const homepageGroupId = await getHomepageTranslationGroupId(supabase);
|
|
219
|
+
revalidatePublicPageSlug(
|
|
220
|
+
page.slug,
|
|
221
|
+
!!homepageGroupId && page.translation_group_id === homepageGroupId
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return {};
|
|
225
|
+
}
|
|
226
|
+
|
|
181
227
|
export async function deletePage(pageId: number) {
|
|
182
228
|
const supabase = createClient();
|
|
183
229
|
|
|
@@ -195,6 +241,13 @@ export async function deletePage(pageId: number) {
|
|
|
195
241
|
|
|
196
242
|
const { translation_group_id } = page;
|
|
197
243
|
|
|
244
|
+
// Resolve whether this is the homepage BEFORE deleting the group rows — once
|
|
245
|
+
// the default-language "home" page is gone, the lookup would return null and a
|
|
246
|
+
// homepage with a non-literal slug wouldn't get "/" revalidated.
|
|
247
|
+
const deleteHomepageGroupId = await getHomepageTranslationGroupId(supabase);
|
|
248
|
+
const deleteIsHomepage =
|
|
249
|
+
!!deleteHomepageGroupId && translation_group_id === deleteHomepageGroupId;
|
|
250
|
+
|
|
198
251
|
// 2. Find All Related Pages
|
|
199
252
|
const { data: relatedPages, error: relatedPagesError } = await supabase
|
|
200
253
|
.from("pages")
|
|
@@ -239,7 +292,7 @@ export async function deletePage(pageId: number) {
|
|
|
239
292
|
revalidatePath("/cms/navigation");
|
|
240
293
|
if (relatedPages) {
|
|
241
294
|
relatedPages.forEach(p => {
|
|
242
|
-
revalidatePublicPageSlug(p.slug);
|
|
295
|
+
revalidatePublicPageSlug(p.slug, deleteIsHomepage);
|
|
243
296
|
});
|
|
244
297
|
}
|
|
245
298
|
|
|
@@ -3,13 +3,13 @@ import React from "react";
|
|
|
3
3
|
import { Separator } from "@nextblock-cms/ui";
|
|
4
4
|
import { createClient } from "@nextblock-cms/db/server";
|
|
5
5
|
import PostForm from "../../components/PostForm";
|
|
6
|
-
import { updatePost } from "../../actions";
|
|
6
|
+
import { updatePost, publishPost } from "../../actions";
|
|
7
7
|
import type { Database } from "@nextblock-cms/db";
|
|
8
8
|
import { notFound, redirect } from "next/navigation";
|
|
9
9
|
import BlockEditorArea from "../../../blocks/components/BlockEditorArea";
|
|
10
10
|
import Link from "next/link";
|
|
11
|
-
import { Button } from "@nextblock-cms/ui";
|
|
12
|
-
import { ArrowLeft,
|
|
11
|
+
import { Button, ViewLiveButton } from "@nextblock-cms/ui";
|
|
12
|
+
import { ArrowLeft, FilePenLine } from "lucide-react";
|
|
13
13
|
import ContentLanguageSwitcher from "../../../components/ContentLanguageSwitcher";
|
|
14
14
|
import { getActiveLanguagesServerSide } from "@nextblock-cms/db/server";
|
|
15
15
|
import { normalizeContentDraftRow } from "../../../../../lib/visual-editing/draft-content";
|
|
@@ -30,7 +30,7 @@ interface PostWithBlocks extends PostType {
|
|
|
30
30
|
translation_group_id: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
async function getPostDataWithBlocks(id: number): Promise<{ post: PostWithBlocks; hasDraft: boolean } | null> {
|
|
33
|
+
async function getPostDataWithBlocks(id: number): Promise<{ post: PostWithBlocks; hasDraft: boolean; liveStatus: string; liveSlug: string } | null> {
|
|
34
34
|
const supabase = createClient();
|
|
35
35
|
const { data: postData, error: postError } = await supabase
|
|
36
36
|
.from("posts")
|
|
@@ -86,7 +86,14 @@ async function getPostDataWithBlocks(id: number): Promise<{ post: PostWithBlocks
|
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
return {
|
|
89
|
+
return {
|
|
90
|
+
post: postWithBlocks,
|
|
91
|
+
hasDraft,
|
|
92
|
+
// The LIVE row's status/slug (before the draft overlay above) — the "View
|
|
93
|
+
// Live" button must reflect what is actually published, not the draft.
|
|
94
|
+
liveStatus: postData.status as string,
|
|
95
|
+
liveSlug: postData.slug as string,
|
|
96
|
+
};
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
export default async function EditPostPage(props: { params: Promise<{ id: string }> }) {
|
|
@@ -113,7 +120,7 @@ export default async function EditPostPage(props: { params: Promise<{ id: string
|
|
|
113
120
|
return notFound();
|
|
114
121
|
}
|
|
115
122
|
|
|
116
|
-
const { post: postWithBlocks, hasDraft } = postDataResult;
|
|
123
|
+
const { post: postWithBlocks, hasDraft, liveStatus, liveSlug } = postDataResult;
|
|
117
124
|
|
|
118
125
|
let initialFeatureImageUrl: string | null = null;
|
|
119
126
|
let initialFeatureImageIdProp: string | null = null;
|
|
@@ -138,6 +145,8 @@ export default async function EditPostPage(props: { params: Promise<{ id: string
|
|
|
138
145
|
const updatePostWithId = updatePost.bind(null, postId);
|
|
139
146
|
const publicPostUrl = `/article/${postWithBlocks.slug}`;
|
|
140
147
|
const draftModeUrl = `/api/draft/start?path=${encodeURIComponent(publicPostUrl)}`;
|
|
148
|
+
const isLive = liveStatus === "published";
|
|
149
|
+
const liveViewUrl = `/article/${liveSlug}`;
|
|
141
150
|
|
|
142
151
|
return (
|
|
143
152
|
<UploadFolderProvider defaultFolder={`posts/${postWithBlocks.slug}/`}>
|
|
@@ -181,11 +190,12 @@ export default async function EditPostPage(props: { params: Promise<{ id: string
|
|
|
181
190
|
allSiteLanguages={allSiteLanguages}
|
|
182
191
|
/>
|
|
183
192
|
)}
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
193
|
+
<ViewLiveButton
|
|
194
|
+
href={liveViewUrl}
|
|
195
|
+
isLive={isLive}
|
|
196
|
+
label="post"
|
|
197
|
+
publishAction={publishPost.bind(null, postId)}
|
|
198
|
+
/>
|
|
189
199
|
<Button variant="secondary" asChild>
|
|
190
200
|
<a href={draftModeUrl} target="_blank" rel="noopener noreferrer">
|
|
191
201
|
<FilePenLine className="mr-2 h-4 w-4" />
|
|
@@ -12,6 +12,51 @@ type PageStatus = Database['public']['Enums']['page_status'];
|
|
|
12
12
|
import { encodedRedirect } from "@nextblock-cms/utils/server"; // Ensure this is correctly imported
|
|
13
13
|
// --- createPost and updatePost functions to be updated similarly for error returns ---
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Publish a post directly (status -> "published") so it becomes visible on the
|
|
17
|
+
* live site. Used by the draft-aware "View Live" button when an admin chooses to
|
|
18
|
+
* publish a still-draft post.
|
|
19
|
+
*/
|
|
20
|
+
export async function publishPost(postId: number): Promise<{ error?: string } | void> {
|
|
21
|
+
const supabase = createClient();
|
|
22
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
23
|
+
if (!user) return { error: "User not authenticated." };
|
|
24
|
+
|
|
25
|
+
const nowIso = new Date().toISOString();
|
|
26
|
+
const { data: existing } = await supabase
|
|
27
|
+
.from("posts")
|
|
28
|
+
.select("published_at")
|
|
29
|
+
.eq("id", postId)
|
|
30
|
+
.maybeSingle();
|
|
31
|
+
|
|
32
|
+
// Publishing "now": if the post has no publish date or a future-scheduled one,
|
|
33
|
+
// set it to now so it isn't withheld by the public "published_at <= now" filter.
|
|
34
|
+
const currentPublishedAt = existing?.published_at;
|
|
35
|
+
const setPublishedNow =
|
|
36
|
+
!currentPublishedAt || new Date(currentPublishedAt).getTime() > Date.now();
|
|
37
|
+
|
|
38
|
+
const { data: post, error } = await supabase
|
|
39
|
+
.from("posts")
|
|
40
|
+
.update({
|
|
41
|
+
status: "published",
|
|
42
|
+
updated_at: nowIso,
|
|
43
|
+
...(setPublishedNow ? { published_at: nowIso } : {}),
|
|
44
|
+
})
|
|
45
|
+
.eq("id", postId)
|
|
46
|
+
.select("slug")
|
|
47
|
+
.single();
|
|
48
|
+
|
|
49
|
+
if (error || !post) {
|
|
50
|
+
return { error: error?.message || "Could not publish the post." };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
revalidatePath("/cms/posts");
|
|
54
|
+
revalidatePath(`/cms/posts/${postId}/edit`);
|
|
55
|
+
revalidatePath(`/article/${post.slug}`);
|
|
56
|
+
revalidatePath("/articles");
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
|
|
15
60
|
export async function createPost(formData: FormData) {
|
|
16
61
|
const supabase = createClient();
|
|
17
62
|
const { data: { user } } = await supabase.auth.getUser();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { verifyPackageOnline, getActiveLanguagesServerSide } from '@nextblock-cms/db/server';
|
|
2
2
|
import { redirect, notFound } from 'next/navigation';
|
|
3
3
|
import Link from 'next/link';
|
|
4
|
-
import { ArrowLeft, ChevronDown
|
|
4
|
+
import { ArrowLeft, ChevronDown } from 'lucide-react';
|
|
5
5
|
import {
|
|
6
6
|
Badge,
|
|
7
7
|
Button,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
DropdownMenuLabel,
|
|
12
12
|
DropdownMenuSeparator,
|
|
13
13
|
DropdownMenuTrigger,
|
|
14
|
+
ViewLiveButton,
|
|
14
15
|
} from '@nextblock-cms/ui';
|
|
15
16
|
import ProductFormClientShell from '../../ProductFormClientShell';
|
|
16
17
|
import {
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
getStoreConfigStatus,
|
|
22
23
|
normalizeCurrencyRecord,
|
|
23
24
|
updateProductAction,
|
|
25
|
+
publishProductAction,
|
|
24
26
|
getCategoriesWithCount,
|
|
25
27
|
getProductCategories,
|
|
26
28
|
} from '@nextblock-cms/ecommerce/server';
|
|
@@ -265,14 +267,14 @@ export default async function EditProductPage({
|
|
|
265
267
|
</DropdownMenu>
|
|
266
268
|
) : null}
|
|
267
269
|
|
|
268
|
-
{product.slug
|
|
269
|
-
<
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
)}
|
|
270
|
+
{product.slug ? (
|
|
271
|
+
<ViewLiveButton
|
|
272
|
+
href={`/product/${product.slug}`}
|
|
273
|
+
isLive={product.status === 'active'}
|
|
274
|
+
label="product"
|
|
275
|
+
publishAction={publishProductAction.bind(null, product.id)}
|
|
276
|
+
/>
|
|
277
|
+
) : null}
|
|
276
278
|
</div>
|
|
277
279
|
</div>
|
|
278
280
|
|