create-nextblock 0.14.2 → 0.14.5

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 (68) hide show
  1. package/package.json +1 -1
  2. package/templates/nextblock-template/app/[slug]/page.tsx +7 -2
  3. package/templates/nextblock-template/app/[slug]/page.utils.ts +8 -3
  4. package/templates/nextblock-template/app/actions/postActions.ts +3 -0
  5. package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -0
  6. package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +83 -3
  7. package/templates/nextblock-template/app/actions/visualEditingActions.ts +34 -14
  8. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +45 -0
  9. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +736 -117
  10. package/templates/nextblock-template/app/api/view/route.ts +114 -0
  11. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +1 -2
  12. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +2 -2
  13. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +17 -22
  14. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +13 -19
  15. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +45 -34
  16. package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +10 -0
  17. package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -0
  18. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +528 -0
  19. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +33 -17
  20. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +19 -7
  21. package/templates/nextblock-template/app/cms/pages/actions.ts +17 -10
  22. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +7 -29
  23. package/templates/nextblock-template/app/cms/pages/page.tsx +6 -19
  24. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +42 -18
  25. package/templates/nextblock-template/app/cms/posts/actions.ts +16 -27
  26. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +3 -60
  27. package/templates/nextblock-template/app/cms/posts/page.tsx +6 -13
  28. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +63 -9
  29. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +66 -32
  30. package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -285
  31. package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -0
  32. package/templates/nextblock-template/app/cms/revisions/service.ts +549 -471
  33. package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -132
  34. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -0
  35. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -0
  36. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -24
  37. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -0
  38. package/templates/nextblock-template/app/layout.tsx +49 -0
  39. package/templates/nextblock-template/app/lib/sitemap-utils.ts +6 -4
  40. package/templates/nextblock-template/app/lib/ucp/server.ts +4 -1
  41. package/templates/nextblock-template/app/page.tsx +6 -3
  42. package/templates/nextblock-template/app/product/[slug]/page.tsx +27 -3
  43. package/templates/nextblock-template/app/providers.tsx +16 -4
  44. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +7 -10
  45. package/templates/nextblock-template/components/theme-icon.tsx +78 -0
  46. package/templates/nextblock-template/components/theme-switcher.tsx +85 -90
  47. package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +4 -1
  48. package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -0
  49. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -0
  50. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +7 -2
  51. package/templates/nextblock-template/lib/blocks/blockColors.test.ts +114 -0
  52. package/templates/nextblock-template/lib/blocks/blockColors.ts +132 -0
  53. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +17 -1
  54. package/templates/nextblock-template/lib/cms-transfer/server.ts +13 -0
  55. package/templates/nextblock-template/lib/full-backup/server.ts +1 -0
  56. package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -0
  57. package/templates/nextblock-template/lib/search/server.ts +3 -0
  58. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +102 -87
  59. package/templates/nextblock-template/lib/themes/buildThemeCss.test.ts +163 -0
  60. package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -0
  61. package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -0
  62. package/templates/nextblock-template/lib/themes/tokens.ts +143 -0
  63. package/templates/nextblock-template/lib/visual-editing/mutations.ts +4 -1
  64. package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +46 -1
  65. package/templates/nextblock-template/next-env.d.ts +2 -2
  66. package/templates/nextblock-template/package.json +1 -1
  67. package/templates/nextblock-template/scripts/verify-site-themes.ts +63 -0
  68. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
@@ -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 } from 'lucide-react';
4
+ import { ArrowLeft, ChevronDown, Eye, FilePenLine } from 'lucide-react';
5
5
  import {
6
6
  Badge,
7
7
  Button,
@@ -11,7 +11,6 @@ import {
11
11
  DropdownMenuLabel,
12
12
  DropdownMenuSeparator,
13
13
  DropdownMenuButtonTrigger,
14
- ViewLiveButton,
15
14
  } from '@nextblock-cms/ui';
16
15
  import ProductFormClientShell from '../../ProductFormClientShell';
17
16
  import {
@@ -22,7 +21,6 @@ import {
22
21
  getStoreConfigStatus,
23
22
  normalizeCurrencyRecord,
24
23
  updateProductAction,
25
- publishProductAction,
26
24
  getCategoriesWithCount,
27
25
  getProductCategories,
28
26
  } from '@nextblock-cms/ecommerce/server';
@@ -33,6 +31,9 @@ import {
33
31
  } from '../../productFormData';
34
32
  import { CortexAiPageContextRegistrar } from '../../../components/CortexAiPageContext';
35
33
  import BlockEditorArea from '../../../blocks/components/BlockEditorArea';
34
+ import VisibilityControl from '../../../components/VisibilityControl';
35
+ import RevisionHistoryButton from '../../../revisions/RevisionHistoryButton';
36
+ import { buildViewUrl } from '../../../../../lib/publishing/viewUrl';
36
37
 
37
38
  export default async function EditProductPage({
38
39
  params,
@@ -112,6 +113,17 @@ export default async function EditProductPage({
112
113
  const buildTranslationCreateHref = (languageId: number) =>
113
114
  `/cms/products/new?from_group=${product.translation_group_id}&target_lang_id=${languageId}`;
114
115
  const globalAttributes = buildGlobalAttributesForForm(globalAttributesRaw || []);
116
+ const productLanguage = languages.find((language) => language.id === product.language_id);
117
+
118
+ // The Status select used to sit inside ProductForm, where submitting with
119
+ // status "active" was blocked unless the payment provider was enabled and
120
+ // configured. Publishing now happens outside the form, so that guard has to
121
+ // travel with it — a product nobody can pay for should not reach the storefront.
122
+ const productProvider = product.payment_provider as 'stripe' | 'freemius' | undefined;
123
+ const publishBlockedReason =
124
+ productProvider && (!enabledProviders[productProvider] || !configStatus[productProvider].hasKeys)
125
+ ? `${productProvider === 'stripe' ? 'Stripe' : 'Freemius'} must be enabled and fully configured before this product can be published.`
126
+ : null;
115
127
 
116
128
  const { data: draftData } = await supabase
117
129
  .from('product_drafts')
@@ -270,13 +282,55 @@ export default async function EditProductPage({
270
282
  ) : null}
271
283
 
272
284
  {product.slug ? (
273
- <ViewLiveButton
274
- href={`/product/${product.slug}`}
275
- isLive={product.status === 'active'}
276
- label="product"
277
- publishAction={publishProductAction.bind(null, product.id)}
278
- />
285
+ <>
286
+ <Button variant="secondary" asChild>
287
+ <a
288
+ href={buildViewUrl({
289
+ path: `/product/${product.slug}`,
290
+ languageCode: productLanguage?.code ?? null,
291
+ draft: true,
292
+ })}
293
+ target="_blank"
294
+ rel="noopener noreferrer"
295
+ >
296
+ <FilePenLine className="mr-2 h-4 w-4" /> Preview
297
+ </a>
298
+ </Button>
299
+ {product.status === 'active' ? (
300
+ <Button variant="outline" asChild>
301
+ <a
302
+ href={buildViewUrl({
303
+ path: `/product/${product.slug}`,
304
+ languageCode: productLanguage?.code ?? null,
305
+ })}
306
+ target="_blank"
307
+ rel="noopener noreferrer"
308
+ >
309
+ <Eye className="mr-2 h-4 w-4" /> View Live
310
+ </a>
311
+ </Button>
312
+ ) : null}
313
+ </>
279
314
  ) : null}
315
+
316
+ <RevisionHistoryButton parentType="product" parentId={product.id} />
317
+
318
+ <VisibilityControl
319
+ type="product"
320
+ id={product.id}
321
+ status={product.status}
322
+ publishedAt={(product as { published_at?: string | null }).published_at ?? null}
323
+ publicPath={`/product/${product.slug}`}
324
+ languageName={
325
+ productLanguage
326
+ ? `${productLanguage.name} (${productLanguage.code.toUpperCase()})`
327
+ : undefined
328
+ }
329
+ translationGroupId={product.translation_group_id}
330
+ languages={languages}
331
+ hasDraft={hasDraft}
332
+ publishBlockedReason={publishBlockedReason}
333
+ />
280
334
  </div>
281
335
  </div>
282
336
 
@@ -12,17 +12,28 @@ import {
12
12
  DialogHeader,
13
13
  DialogTitle,
14
14
  } from "@nextblock-cms/ui";
15
- import { listPageRevisions, listPostRevisions, restorePageVersion, restorePostVersion, comparePageVersion, comparePostVersion } from './actions';
15
+ import {
16
+ listPageRevisions,
17
+ listPostRevisions,
18
+ listProductRevisions,
19
+ restorePageVersion,
20
+ restorePostVersion,
21
+ restoreProductVersion,
22
+ comparePageVersion,
23
+ comparePostVersion,
24
+ compareProductVersion,
25
+ } from './actions';
16
26
  import { useRouter } from 'next/navigation';
17
27
  import JsonDiffView from './JsonDiffView';
18
28
  import { formatDistanceToNow } from 'date-fns';
19
29
  import { toast } from 'react-hot-toast';
20
30
 
21
- type ParentType = 'page' | 'post';
31
+ type ParentType = 'page' | 'post' | 'product';
22
32
 
23
33
  interface RevisionHistoryButtonProps {
24
34
  parentType: ParentType;
25
- parentId: number;
35
+ /** Pages and posts are keyed by bigint; products by uuid. */
36
+ parentId: number | string;
26
37
  }
27
38
 
28
39
  type RevisionItem = {
@@ -32,6 +43,28 @@ type RevisionItem = {
32
43
  created_at: string;
33
44
  author_id: string | null;
34
45
  author?: { full_name?: string | null } | null;
46
+ has_changes?: boolean;
47
+ };
48
+
49
+ type ListResult = { error: string } | { revisions: unknown[]; currentVersion: number | null; totalPages: number };
50
+ type RestoreResult = { error: string } | { success: true; version: number };
51
+ type CompareResult = { success: true; current: unknown; target: unknown } | { error: string };
52
+
53
+ /**
54
+ * Exhaustive dispatch, one entry per parent type.
55
+ *
56
+ * Deliberately a lookup rather than an `if page / else` chain: with three parent types a
57
+ * bare `else` silently routes products into the post actions, which type-checks cleanly
58
+ * because both take an id and a version.
59
+ */
60
+ const REVISION_API: Record<ParentType, {
61
+ list: (id: any, page: number, start?: string, end?: string) => Promise<ListResult>;
62
+ restore: (id: any, version: number) => Promise<RestoreResult>;
63
+ compare: (id: any, version: number) => Promise<CompareResult>;
64
+ }> = {
65
+ page: { list: listPageRevisions, restore: restorePageVersion, compare: comparePageVersion },
66
+ post: { list: listPostRevisions, restore: restorePostVersion, compare: comparePostVersion },
67
+ product: { list: listProductRevisions, restore: restoreProductVersion, compare: compareProductVersion },
35
68
  };
36
69
 
37
70
  import { Input } from "@nextblock-cms/ui";
@@ -53,20 +86,17 @@ export default function RevisionHistoryButton({ parentType, parentId }: Revision
53
86
  const router = useRouter();
54
87
  const [compareLoading, setCompareLoading] = useState(false);
55
88
  const [activeCompareVersion, setActiveCompareVersion] = useState<number | null>(null);
89
+ const [restoringVersion, setRestoringVersion] = useState<number | null>(null);
56
90
  const [leftText, setLeftText] = useState<string | null>(null);
57
91
  const [rightText, setRightText] = useState<string | null>(null);
58
92
 
93
+ const api = REVISION_API[parentType];
94
+
59
95
  const loadRevisions = async (pageToLoad: number, start?: string, end?: string) => {
60
96
  setLoading(true);
61
97
  setError(null);
62
98
  try {
63
- let res;
64
- // Pass date range to actions
65
- if (parentType === 'page') {
66
- res = await listPageRevisions(parentId, pageToLoad, start, end);
67
- } else {
68
- res = await listPostRevisions(parentId, pageToLoad, start, end);
69
- }
99
+ const res = await api.list(parentId, pageToLoad, start, end);
70
100
 
71
101
  if ('error' in res) {
72
102
  setError(res.error ?? 'Unknown error');
@@ -122,37 +152,35 @@ export default function RevisionHistoryButton({ parentType, parentId }: Revision
122
152
  const handleRestore = (version: number) => {
123
153
  setMessage(null);
124
154
  setError(null);
155
+ setRestoringVersion(version);
125
156
  startTransition(async () => {
126
157
  try {
127
- const res = parentType === 'page'
128
- ? await restorePageVersion(parentId, version)
129
- : await restorePostVersion(parentId, version);
158
+ const res = await api.restore(parentId, version);
130
159
  if ('error' in res) {
131
160
  setError(res.error ?? 'Unknown error');
132
161
  toast.error(res.error ?? 'Failed to restore version');
133
162
  return;
134
163
  }
135
- setMessage('Version restored successfully.');
164
+ setMessage(`Version ${version} restored as version ${res.version}.`);
136
165
  toast.success('Version restored successfully');
137
166
  router.refresh();
138
167
  setTimeout(() => setOpen(false), 800);
139
168
  } catch (e: unknown) {
140
169
  setError(e instanceof Error ? e.message : 'Failed to restore version');
141
170
  toast.error(e instanceof Error ? e.message : 'Failed to restore version');
171
+ } finally {
172
+ setRestoringVersion(null);
142
173
  }
143
174
  });
144
175
  };
145
176
 
146
- type CompareResult = { success: true; current: unknown; target: unknown } | { error: string };
147
177
  const handleCompare = async (version: number) => {
148
178
  setError(null);
149
179
  setMessage(null);
150
180
  setActiveCompareVersion(version);
151
181
  setCompareLoading(true);
152
182
  try {
153
- const res: CompareResult = parentType === 'page'
154
- ? await comparePageVersion(parentId, version)
155
- : await comparePostVersion(parentId, version);
183
+ const res = await api.compare(parentId, version);
156
184
  if ('error' in res) {
157
185
  setError(res.error);
158
186
  setLeftText(null);
@@ -245,25 +273,31 @@ export default function RevisionHistoryButton({ parentType, parentId }: Revision
245
273
  </div>
246
274
 
247
275
  <div className="flex gap-2">
248
- {!isCurrent && (
276
+ {/* A row with no differences against the live content is labelled, not
277
+ hidden: `invisible` left an interactive-looking blank where the
278
+ buttons should be, which reads as a broken dialog. */}
279
+ {!isCurrent && rev.has_changes === false && (
280
+ <span className="text-xs text-muted-foreground italic px-2 self-center">Identical to current</span>
281
+ )}
282
+ {!isCurrent && rev.has_changes !== false && (
249
283
  <>
250
- <Button
251
- variant="outline"
252
- size="sm"
253
- onClick={() => handleCompare(rev.version)}
254
- disabled={compareLoading && activeCompareVersion === rev.version || (rev as any).has_changes === false}
255
- className={cn("h-8 px-2 text-xs text-blue-600 border-blue-200 hover:bg-blue-50 hover:text-blue-700", (rev as any).has_changes === false && "invisible pointer-events-none")}
284
+ <Button
285
+ variant="outline"
286
+ size="sm"
287
+ onClick={() => handleCompare(rev.version)}
288
+ disabled={compareLoading && activeCompareVersion === rev.version}
289
+ className="h-8 px-2 text-xs text-blue-600 border-blue-200 hover:bg-blue-50 hover:text-blue-700"
256
290
  >
257
291
  {compareLoading && activeCompareVersion === rev.version ? <><Spinner className="mr-1 h-3 w-3" /> compare</> : 'Compare'}
258
292
  </Button>
259
- <Button
260
- variant="destructive"
261
- size="sm"
262
- onClick={() => handleRestore(rev.version)}
263
- disabled={isPending || (rev as any).has_changes === false}
264
- className={cn("h-8 px-2 text-xs", (rev as any).has_changes === false && "invisible pointer-events-none")}
293
+ <Button
294
+ variant="destructive"
295
+ size="sm"
296
+ onClick={() => handleRestore(rev.version)}
297
+ disabled={isPending}
298
+ className="h-8 px-2 text-xs"
265
299
  >
266
- {isPending ? <Spinner className="h-3 w-3" /> : 'Restore'}
300
+ {isPending && restoringVersion === rev.version ? <Spinner className="h-3 w-3" /> : 'Restore'}
267
301
  </Button>
268
302
  </>
269
303
  )}