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.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/templates/nextblock-template/app/actions/interactions.ts +27 -4
  3. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +287 -48
  4. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +238 -209
  5. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +103 -8
  6. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +31 -2
  7. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +37 -15
  8. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +26 -15
  9. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +123 -46
  10. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +8 -1
  11. package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +62 -22
  12. package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +40 -2
  13. package/templates/nextblock-template/app/cms/interactions/EmailRecipientsInput.tsx +189 -0
  14. package/templates/nextblock-template/app/cms/interactions/InteractionsModerationClient.tsx +138 -71
  15. package/templates/nextblock-template/app/cms/media/import-external-image.ts +289 -0
  16. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +13 -10
  17. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +14 -3
  18. package/templates/nextblock-template/app/cms/pages/actions.ts +59 -6
  19. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +21 -11
  20. package/templates/nextblock-template/app/cms/posts/actions.ts +45 -0
  21. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +11 -9
  22. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +463 -227
  23. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +220 -1
  24. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +11 -0
  25. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +20 -1
  26. package/templates/nextblock-template/app/cms/users/actions.ts +69 -0
  27. package/templates/nextblock-template/app/cms/users/components/CreateUserForm.tsx +217 -0
  28. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +4 -1
  29. package/templates/nextblock-template/app/cms/users/new/page.tsx +44 -0
  30. package/templates/nextblock-template/app/cms/users/page.tsx +12 -3
  31. package/templates/nextblock-template/app/lib/homepage.ts +36 -0
  32. package/templates/nextblock-template/app/lib/sitemap-utils.ts +13 -6
  33. package/templates/nextblock-template/app/page.tsx +55 -12
  34. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +56 -0
  35. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +60 -30
  36. package/templates/nextblock-template/components/blocks/renderers/StockPhotoCredit.tsx +167 -0
  37. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +94 -6
  38. package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +7 -1
  39. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +29 -3
  40. package/templates/nextblock-template/lib/search/server.ts +11 -1
  41. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +82 -72
  42. package/templates/nextblock-template/next-env.d.ts +1 -1
  43. package/templates/nextblock-template/package.json +1 -1
  44. package/templates/nextblock-template/proxy.ts +5 -0
  45. 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(slug: string | null | undefined) {
22
+ function revalidatePublicPageSlug(
23
+ slug: string | null | undefined,
24
+ isHomepage = false
25
+ ) {
22
26
  if (!slug) return;
23
27
 
24
28
  revalidatePath(`/${slug}`);
25
- if (slug === "home" || slug === "accueil") {
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
- revalidatePublicPageSlug(newPage?.slug);
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
- revalidatePublicPageSlug(existingPage.slug);
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, Eye, FilePenLine } from "lucide-react";
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 { post: postWithBlocks, hasDraft };
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
- <Button variant="outline" asChild>
185
- <Link href={publicPostUrl} target="_blank" rel="noopener noreferrer">
186
- <Eye className="mr-2 h-4 w-4" /> View Live Post
187
- </Link>
188
- </Button>
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, ExternalLink } from 'lucide-react';
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 && product.status === 'active' && (
269
- <Button variant="outline" asChild>
270
- <Link href={`/product/${product.slug}`} target="_blank">
271
- <ExternalLink className="w-4 h-4 mr-2" />
272
- View
273
- </Link>
274
- </Button>
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