create-nextblock 0.16.4 → 0.17.1
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/CLAUDE.md +22 -0
- package/docker-template/.env.docker.example +69 -69
- package/docker-template/docker/db/init/99-jwt.sql +6 -6
- package/docker-template/docker/db/init/99-roles.sql +25 -25
- package/docker-template/docker/kong/kong.yml +112 -112
- package/docker-template/scripts/docker-setup.mjs +310 -310
- package/package.json +1 -1
- package/templates/nextblock-template/.browserslistrc +11 -11
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/CLAUDE.md +26 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
- package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
- package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
- package/templates/nextblock-template/app/actions/consent.ts +57 -57
- package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
- package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
- package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
- package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
- package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +272 -205
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +10473 -8467
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
- package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
- package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
- package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
- package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
- package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
- package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
- package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +442 -441
- package/templates/nextblock-template/app/article/[slug]/page.utils.ts +4 -1
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
- package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
- package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
- package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +3 -3
- package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
- package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -136
- package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
- package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
- package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
- package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
- package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
- package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
- package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
- package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
- package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
- package/templates/nextblock-template/app/cms/pages/page.tsx +273 -273
- package/templates/nextblock-template/app/cms/posts/page.tsx +251 -251
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
- package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
- package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
- package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
- package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
- package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
- package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
- package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
- package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
- package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
- package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
- package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
- package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
- package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
- package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
- package/templates/nextblock-template/app/setup/layout.tsx +13 -13
- package/templates/nextblock-template/app/setup/page.tsx +111 -111
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
- package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
- package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
- package/templates/nextblock-template/components/Header.tsx +38 -38
- package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
- package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
- package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
- package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +11 -0
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
- package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/media/YouTubeFacade.tsx +105 -105
- package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -32
- package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -471
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
- package/templates/nextblock-template/context/language-rest-client.ts +32 -32
- package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
- package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
- package/templates/nextblock-template/docker/kong/kong.yml +112 -112
- package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +1 -1
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +402 -282
- package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +15 -8
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +23 -4
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
- package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +4 -1
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +8 -0
- package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +2 -2
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +95 -104
- package/templates/nextblock-template/lib/app-secrets.ts +39 -39
- package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
- package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
- package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
- package/templates/nextblock-template/lib/blocks/README.md +13 -13
- package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
- package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -135
- package/templates/nextblock-template/lib/cms/payments-reminder.ts +105 -105
- package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
- package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
- package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
- package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
- package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
- package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
- package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
- package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
- package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
- package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
- package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
- package/templates/nextblock-template/lib/email/branding.ts +76 -76
- package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
- package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
- package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +56 -54
- package/templates/nextblock-template/lib/media/youtube.ts +99 -99
- package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
- package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
- package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
- package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
- package/templates/nextblock-template/lib/privacy/types.ts +69 -69
- package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
- package/templates/nextblock-template/lib/promotions/server.ts +741 -741
- package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
- package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
- package/templates/nextblock-template/lib/seo/page-document.ts +4 -4
- package/templates/nextblock-template/lib/seo/redirect-store.ts +3 -2
- package/templates/nextblock-template/lib/setup/actions.ts +460 -460
- package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
- package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +15 -175
- package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
- package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
- package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
- package/templates/nextblock-template/lib/setup/types.ts +18 -18
- package/templates/nextblock-template/lib/storage/provider.ts +66 -66
- package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
- package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
- package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
- package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
- package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
- package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
- package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
- package/templates/nextblock-template/lib/zod-config.ts +5 -5
- package/templates/nextblock-template/next-env.d.ts +1 -0
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/public/images/cortex_post.webp +0 -0
- package/templates/nextblock-template/public/images/update_nextblock.webp +0 -0
- package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
- package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
- package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
- package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
- package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
- package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
- package/templates/nextblock-template/types/jsdom.d.ts +6 -6
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
import 'server-only';
|
|
2
|
-
|
|
3
|
-
import { createClient, verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
4
|
-
import { getStoreReadiness } from '@nextblock-cms/ecommerce/server';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* A store that has commerce switched on but cannot actually take money. This is easy to
|
|
8
|
-
* end up in — the products, prices and pages all look finished — and the only symptom
|
|
9
|
-
* used to be a shopper hitting a payment error at checkout. The banner makes the gap
|
|
10
|
-
* visible in the CMS instead of on the storefront.
|
|
11
|
-
*/
|
|
12
|
-
export interface PaymentsReminder {
|
|
13
|
-
/** Providers that are not ready, with what each is missing. */
|
|
14
|
-
blocked: Array<{
|
|
15
|
-
provider: 'stripe' | 'freemius';
|
|
16
|
-
label: string;
|
|
17
|
-
missing: string[];
|
|
18
|
-
}>;
|
|
19
|
-
/** Published products that currently cannot be bought because of the above. */
|
|
20
|
-
affectedProducts: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Best-effort reminder state for the CMS layout. Returns null (no banner) whenever
|
|
25
|
-
* nagging would be wrong or the answer can't be determined — an unmigrated database,
|
|
26
|
-
* an instance without commerce, or the sandbox.
|
|
27
|
-
*/
|
|
28
|
-
export async function getPaymentsReminder(): Promise<PaymentsReminder | null> {
|
|
29
|
-
// The sandbox refuses to store live credentials at all (savePaymentCredentials throws),
|
|
30
|
-
// so a "finish setting up payments" nag there is noise the operator cannot act on.
|
|
31
|
-
if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') return null;
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
if (!(await verifyPackageOnline('ecommerce'))) return null;
|
|
35
|
-
|
|
36
|
-
const supabase = createClient();
|
|
37
|
-
|
|
38
|
-
// Only ADMINs can act on this (the /cms/payments route is admin-only), so don't do
|
|
39
|
-
// the work for anyone else.
|
|
40
|
-
const {
|
|
41
|
-
data: { user },
|
|
42
|
-
} = await supabase.auth.getUser();
|
|
43
|
-
if (!user) return null;
|
|
44
|
-
const { data: profile } = await supabase
|
|
45
|
-
.from('profiles')
|
|
46
|
-
.select('role')
|
|
47
|
-
.eq('id', user.id)
|
|
48
|
-
.maybeSingle();
|
|
49
|
-
if (profile?.role !== 'ADMIN') return null;
|
|
50
|
-
|
|
51
|
-
const readiness = await getStoreReadiness();
|
|
52
|
-
const unready = (['stripe', 'freemius'] as const)
|
|
53
|
-
.map((provider) => readiness[provider])
|
|
54
|
-
.filter((entry) => !entry.ready);
|
|
55
|
-
|
|
56
|
-
if (unready.length === 0) return null;
|
|
57
|
-
|
|
58
|
-
// A provider matters to this reminder only when the catalogue contains products
|
|
59
|
-
// of the type routed through it. Do not treat a stale/accidental provider toggle as
|
|
60
|
-
// usage: that produced a Stripe warning in digital-only stores with no Stripe keys.
|
|
61
|
-
// Count across ALL statuses so a shop building a draft catalogue is warned before
|
|
62
|
-
// it publishes rather than after.
|
|
63
|
-
const usage = await Promise.all(
|
|
64
|
-
unready.map(async (entry) => {
|
|
65
|
-
const productType =
|
|
66
|
-
entry.provider === 'stripe' ? 'physical' : 'digital';
|
|
67
|
-
const [{ count: totalCount }, { count: activeCount }] =
|
|
68
|
-
await Promise.all([
|
|
69
|
-
supabase
|
|
70
|
-
.from('products')
|
|
71
|
-
.select('id', { count: 'exact', head: true })
|
|
72
|
-
.eq('product_type', productType),
|
|
73
|
-
supabase
|
|
74
|
-
.from('products')
|
|
75
|
-
.select('id', { count: 'exact', head: true })
|
|
76
|
-
.eq('status', 'active')
|
|
77
|
-
.eq('product_type', productType),
|
|
78
|
-
]);
|
|
79
|
-
return {
|
|
80
|
-
entry,
|
|
81
|
-
inUse: (totalCount ?? 0) > 0,
|
|
82
|
-
activeProducts: activeCount ?? 0,
|
|
83
|
-
};
|
|
84
|
-
}),
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
const relevant = usage.filter((item) => item.inUse);
|
|
88
|
-
if (relevant.length === 0) return null;
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
blocked: relevant.map(({ entry }) => ({
|
|
92
|
-
provider: entry.provider,
|
|
93
|
-
label: entry.label,
|
|
94
|
-
missing: entry.missing,
|
|
95
|
-
})),
|
|
96
|
-
affectedProducts: relevant.reduce(
|
|
97
|
-
(total, item) => total + item.activeProducts,
|
|
98
|
-
0,
|
|
99
|
-
),
|
|
100
|
-
};
|
|
101
|
-
} catch {
|
|
102
|
-
// Never let a reminder break the CMS shell.
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
1
|
+
import 'server-only';
|
|
2
|
+
|
|
3
|
+
import { createClient, verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
4
|
+
import { getStoreReadiness } from '@nextblock-cms/ecommerce/server';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A store that has commerce switched on but cannot actually take money. This is easy to
|
|
8
|
+
* end up in — the products, prices and pages all look finished — and the only symptom
|
|
9
|
+
* used to be a shopper hitting a payment error at checkout. The banner makes the gap
|
|
10
|
+
* visible in the CMS instead of on the storefront.
|
|
11
|
+
*/
|
|
12
|
+
export interface PaymentsReminder {
|
|
13
|
+
/** Providers that are not ready, with what each is missing. */
|
|
14
|
+
blocked: Array<{
|
|
15
|
+
provider: 'stripe' | 'freemius';
|
|
16
|
+
label: string;
|
|
17
|
+
missing: string[];
|
|
18
|
+
}>;
|
|
19
|
+
/** Published products that currently cannot be bought because of the above. */
|
|
20
|
+
affectedProducts: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Best-effort reminder state for the CMS layout. Returns null (no banner) whenever
|
|
25
|
+
* nagging would be wrong or the answer can't be determined — an unmigrated database,
|
|
26
|
+
* an instance without commerce, or the sandbox.
|
|
27
|
+
*/
|
|
28
|
+
export async function getPaymentsReminder(): Promise<PaymentsReminder | null> {
|
|
29
|
+
// The sandbox refuses to store live credentials at all (savePaymentCredentials throws),
|
|
30
|
+
// so a "finish setting up payments" nag there is noise the operator cannot act on.
|
|
31
|
+
if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') return null;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
if (!(await verifyPackageOnline('ecommerce'))) return null;
|
|
35
|
+
|
|
36
|
+
const supabase = createClient();
|
|
37
|
+
|
|
38
|
+
// Only ADMINs can act on this (the /cms/payments route is admin-only), so don't do
|
|
39
|
+
// the work for anyone else.
|
|
40
|
+
const {
|
|
41
|
+
data: { user },
|
|
42
|
+
} = await supabase.auth.getUser();
|
|
43
|
+
if (!user) return null;
|
|
44
|
+
const { data: profile } = await supabase
|
|
45
|
+
.from('profiles')
|
|
46
|
+
.select('role')
|
|
47
|
+
.eq('id', user.id)
|
|
48
|
+
.maybeSingle();
|
|
49
|
+
if (profile?.role !== 'ADMIN') return null;
|
|
50
|
+
|
|
51
|
+
const readiness = await getStoreReadiness();
|
|
52
|
+
const unready = (['stripe', 'freemius'] as const)
|
|
53
|
+
.map((provider) => readiness[provider])
|
|
54
|
+
.filter((entry) => !entry.ready);
|
|
55
|
+
|
|
56
|
+
if (unready.length === 0) return null;
|
|
57
|
+
|
|
58
|
+
// A provider matters to this reminder only when the catalogue contains products
|
|
59
|
+
// of the type routed through it. Do not treat a stale/accidental provider toggle as
|
|
60
|
+
// usage: that produced a Stripe warning in digital-only stores with no Stripe keys.
|
|
61
|
+
// Count across ALL statuses so a shop building a draft catalogue is warned before
|
|
62
|
+
// it publishes rather than after.
|
|
63
|
+
const usage = await Promise.all(
|
|
64
|
+
unready.map(async (entry) => {
|
|
65
|
+
const productType =
|
|
66
|
+
entry.provider === 'stripe' ? 'physical' : 'digital';
|
|
67
|
+
const [{ count: totalCount }, { count: activeCount }] =
|
|
68
|
+
await Promise.all([
|
|
69
|
+
supabase
|
|
70
|
+
.from('products')
|
|
71
|
+
.select('id', { count: 'exact', head: true })
|
|
72
|
+
.eq('product_type', productType),
|
|
73
|
+
supabase
|
|
74
|
+
.from('products')
|
|
75
|
+
.select('id', { count: 'exact', head: true })
|
|
76
|
+
.eq('status', 'active')
|
|
77
|
+
.eq('product_type', productType),
|
|
78
|
+
]);
|
|
79
|
+
return {
|
|
80
|
+
entry,
|
|
81
|
+
inUse: (totalCount ?? 0) > 0,
|
|
82
|
+
activeProducts: activeCount ?? 0,
|
|
83
|
+
};
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const relevant = usage.filter((item) => item.inUse);
|
|
88
|
+
if (relevant.length === 0) return null;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
blocked: relevant.map(({ entry }) => ({
|
|
92
|
+
provider: entry.provider,
|
|
93
|
+
label: entry.label,
|
|
94
|
+
missing: entry.missing,
|
|
95
|
+
})),
|
|
96
|
+
affectedProducts: relevant.reduce(
|
|
97
|
+
(total, item) => total + item.activeProducts,
|
|
98
|
+
0,
|
|
99
|
+
),
|
|
100
|
+
};
|
|
101
|
+
} catch {
|
|
102
|
+
// Never let a reminder break the CMS shell.
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -1,145 +1,145 @@
|
|
|
1
|
-
import type { Json } from "@nextblock-cms/db";
|
|
2
|
-
|
|
3
|
-
export type CmsContentType = "pages" | "posts" | "products";
|
|
4
|
-
export type CmsImportConflictMode = "create_new" | "overwrite_existing";
|
|
5
|
-
export type CmsImportApplyMode = "draft" | "live";
|
|
6
|
-
|
|
7
|
-
export interface CmsImportOptions {
|
|
8
|
-
conflictMode: CmsImportConflictMode;
|
|
9
|
-
applyMode: CmsImportApplyMode;
|
|
10
|
-
ignoreBlankFields?: boolean;
|
|
11
|
-
skipUnavailableProductContent?: boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface CmsImportMessage {
|
|
15
|
-
row: number;
|
|
16
|
-
type: "error" | "warning";
|
|
17
|
-
message: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface CmsImportPreviewItem {
|
|
21
|
-
row: number;
|
|
22
|
-
action: "create" | "update" | "skip";
|
|
23
|
-
identifier: string;
|
|
24
|
-
contentType: CmsContentType;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface CmsImportSummary {
|
|
28
|
-
success: boolean;
|
|
29
|
-
totalRows: number;
|
|
30
|
-
created: number;
|
|
31
|
-
updated: number;
|
|
32
|
-
skipped: number;
|
|
33
|
-
errors: CmsImportMessage[];
|
|
34
|
-
warnings: CmsImportMessage[];
|
|
35
|
-
preview: CmsImportPreviewItem[];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface CmsTransferActionResult {
|
|
39
|
-
success: boolean;
|
|
40
|
-
error?: string;
|
|
41
|
-
fileName?: string;
|
|
42
|
-
mimeType?: string;
|
|
43
|
-
content?: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface CmsBackupBundleV1 {
|
|
47
|
-
version: 1;
|
|
48
|
-
exported_at: string;
|
|
49
|
-
languages: Array<{
|
|
50
|
-
id: number;
|
|
51
|
-
code: string;
|
|
52
|
-
name: string | null;
|
|
53
|
-
}>;
|
|
54
|
-
content: {
|
|
55
|
-
pages: BackupPageRecord[];
|
|
56
|
-
posts: BackupPostRecord[];
|
|
57
|
-
products: BackupProductRecord[];
|
|
58
|
-
};
|
|
59
|
-
// Optional for backward compatibility with bundles exported before the
|
|
60
|
-
// Blocks Library was added to the content backup.
|
|
61
|
-
custom_blocks?: BackupCustomBlockRecord[];
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface BackupBlockRecord {
|
|
65
|
-
language_id?: number;
|
|
66
|
-
block_type: string;
|
|
67
|
-
content: Json;
|
|
68
|
-
order?: number;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface BackupCustomBlockRecord {
|
|
72
|
-
slug: string;
|
|
73
|
-
name: string;
|
|
74
|
-
description?: string;
|
|
75
|
-
fields: Json;
|
|
76
|
-
layout_schema: Json;
|
|
77
|
-
is_original?: boolean;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface BackupPageRecord {
|
|
81
|
-
id?: number | string | null;
|
|
82
|
-
translation_group_id?: string | null;
|
|
83
|
-
language_code: string;
|
|
84
|
-
title: string;
|
|
85
|
-
slug: string;
|
|
86
|
-
status: string;
|
|
87
|
-
meta_title?: string | null;
|
|
88
|
-
meta_description?: string | null;
|
|
89
|
-
feature_image_id?: string | null;
|
|
90
|
-
feature_image_object_key?: string | null;
|
|
91
|
-
blocks: BackupBlockRecord[];
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export interface BackupPostRecord extends BackupPageRecord {
|
|
95
|
-
label?: string | null;
|
|
96
|
-
excerpt?: string | null;
|
|
97
|
-
subtitle?: string | null;
|
|
98
|
-
published_at?: string | null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface BackupProductVariantRecord {
|
|
102
|
-
id?: string | null;
|
|
103
|
-
sku: string;
|
|
104
|
-
upc?: string | null;
|
|
105
|
-
price: number;
|
|
106
|
-
prices?: Record<string, number>;
|
|
107
|
-
sale_price?: number | null;
|
|
108
|
-
sale_prices?: Record<string, number | null> | null;
|
|
109
|
-
stock_quantity: number;
|
|
110
|
-
main_media_id?: string | null;
|
|
111
|
-
main_media_object_key?: string | null;
|
|
112
|
-
attribute_term_ids?: string[];
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export interface BackupProductRecord {
|
|
116
|
-
id?: string | null;
|
|
117
|
-
translation_group_id?: string | null;
|
|
118
|
-
language_code: string;
|
|
119
|
-
product_type: string;
|
|
120
|
-
payment_provider: string;
|
|
121
|
-
title: string;
|
|
122
|
-
slug: string;
|
|
123
|
-
sku: string;
|
|
124
|
-
upc?: string | null;
|
|
125
|
-
status: string;
|
|
126
|
-
price: number;
|
|
127
|
-
prices?: Record<string, number>;
|
|
128
|
-
sale_price?: number | null;
|
|
129
|
-
sale_prices?: Record<string, number | null> | null;
|
|
130
|
-
stock: number;
|
|
131
|
-
is_taxable: boolean;
|
|
132
|
-
meta_title?: string | null;
|
|
133
|
-
meta_description?: string | null;
|
|
134
|
-
short_description?: string | null;
|
|
135
|
-
description_json?: Json;
|
|
136
|
-
description_blocks: BackupBlockRecord[];
|
|
137
|
-
category_slugs?: string[];
|
|
138
|
-
media_ids?: string[];
|
|
139
|
-
media_object_keys?: string[];
|
|
140
|
-
variants?: BackupProductVariantRecord[];
|
|
141
|
-
freemius_product_id?: string | null;
|
|
142
|
-
freemius_plan_id?: string | null;
|
|
143
|
-
trial_period_days?: number;
|
|
144
|
-
trial_requires_payment_method?: boolean;
|
|
145
|
-
}
|
|
1
|
+
import type { Json } from "@nextblock-cms/db";
|
|
2
|
+
|
|
3
|
+
export type CmsContentType = "pages" | "posts" | "products";
|
|
4
|
+
export type CmsImportConflictMode = "create_new" | "overwrite_existing";
|
|
5
|
+
export type CmsImportApplyMode = "draft" | "live";
|
|
6
|
+
|
|
7
|
+
export interface CmsImportOptions {
|
|
8
|
+
conflictMode: CmsImportConflictMode;
|
|
9
|
+
applyMode: CmsImportApplyMode;
|
|
10
|
+
ignoreBlankFields?: boolean;
|
|
11
|
+
skipUnavailableProductContent?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CmsImportMessage {
|
|
15
|
+
row: number;
|
|
16
|
+
type: "error" | "warning";
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CmsImportPreviewItem {
|
|
21
|
+
row: number;
|
|
22
|
+
action: "create" | "update" | "skip";
|
|
23
|
+
identifier: string;
|
|
24
|
+
contentType: CmsContentType;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CmsImportSummary {
|
|
28
|
+
success: boolean;
|
|
29
|
+
totalRows: number;
|
|
30
|
+
created: number;
|
|
31
|
+
updated: number;
|
|
32
|
+
skipped: number;
|
|
33
|
+
errors: CmsImportMessage[];
|
|
34
|
+
warnings: CmsImportMessage[];
|
|
35
|
+
preview: CmsImportPreviewItem[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CmsTransferActionResult {
|
|
39
|
+
success: boolean;
|
|
40
|
+
error?: string;
|
|
41
|
+
fileName?: string;
|
|
42
|
+
mimeType?: string;
|
|
43
|
+
content?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CmsBackupBundleV1 {
|
|
47
|
+
version: 1;
|
|
48
|
+
exported_at: string;
|
|
49
|
+
languages: Array<{
|
|
50
|
+
id: number;
|
|
51
|
+
code: string;
|
|
52
|
+
name: string | null;
|
|
53
|
+
}>;
|
|
54
|
+
content: {
|
|
55
|
+
pages: BackupPageRecord[];
|
|
56
|
+
posts: BackupPostRecord[];
|
|
57
|
+
products: BackupProductRecord[];
|
|
58
|
+
};
|
|
59
|
+
// Optional for backward compatibility with bundles exported before the
|
|
60
|
+
// Blocks Library was added to the content backup.
|
|
61
|
+
custom_blocks?: BackupCustomBlockRecord[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface BackupBlockRecord {
|
|
65
|
+
language_id?: number;
|
|
66
|
+
block_type: string;
|
|
67
|
+
content: Json;
|
|
68
|
+
order?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface BackupCustomBlockRecord {
|
|
72
|
+
slug: string;
|
|
73
|
+
name: string;
|
|
74
|
+
description?: string;
|
|
75
|
+
fields: Json;
|
|
76
|
+
layout_schema: Json;
|
|
77
|
+
is_original?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface BackupPageRecord {
|
|
81
|
+
id?: number | string | null;
|
|
82
|
+
translation_group_id?: string | null;
|
|
83
|
+
language_code: string;
|
|
84
|
+
title: string;
|
|
85
|
+
slug: string;
|
|
86
|
+
status: string;
|
|
87
|
+
meta_title?: string | null;
|
|
88
|
+
meta_description?: string | null;
|
|
89
|
+
feature_image_id?: string | null;
|
|
90
|
+
feature_image_object_key?: string | null;
|
|
91
|
+
blocks: BackupBlockRecord[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface BackupPostRecord extends BackupPageRecord {
|
|
95
|
+
label?: string | null;
|
|
96
|
+
excerpt?: string | null;
|
|
97
|
+
subtitle?: string | null;
|
|
98
|
+
published_at?: string | null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface BackupProductVariantRecord {
|
|
102
|
+
id?: string | null;
|
|
103
|
+
sku: string;
|
|
104
|
+
upc?: string | null;
|
|
105
|
+
price: number;
|
|
106
|
+
prices?: Record<string, number>;
|
|
107
|
+
sale_price?: number | null;
|
|
108
|
+
sale_prices?: Record<string, number | null> | null;
|
|
109
|
+
stock_quantity: number;
|
|
110
|
+
main_media_id?: string | null;
|
|
111
|
+
main_media_object_key?: string | null;
|
|
112
|
+
attribute_term_ids?: string[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface BackupProductRecord {
|
|
116
|
+
id?: string | null;
|
|
117
|
+
translation_group_id?: string | null;
|
|
118
|
+
language_code: string;
|
|
119
|
+
product_type: string;
|
|
120
|
+
payment_provider: string;
|
|
121
|
+
title: string;
|
|
122
|
+
slug: string;
|
|
123
|
+
sku: string;
|
|
124
|
+
upc?: string | null;
|
|
125
|
+
status: string;
|
|
126
|
+
price: number;
|
|
127
|
+
prices?: Record<string, number>;
|
|
128
|
+
sale_price?: number | null;
|
|
129
|
+
sale_prices?: Record<string, number | null> | null;
|
|
130
|
+
stock: number;
|
|
131
|
+
is_taxable: boolean;
|
|
132
|
+
meta_title?: string | null;
|
|
133
|
+
meta_description?: string | null;
|
|
134
|
+
short_description?: string | null;
|
|
135
|
+
description_json?: Json;
|
|
136
|
+
description_blocks: BackupBlockRecord[];
|
|
137
|
+
category_slugs?: string[];
|
|
138
|
+
media_ids?: string[];
|
|
139
|
+
media_object_keys?: string[];
|
|
140
|
+
variants?: BackupProductVariantRecord[];
|
|
141
|
+
freemius_product_id?: string | null;
|
|
142
|
+
freemius_plan_id?: string | null;
|
|
143
|
+
trial_period_days?: number;
|
|
144
|
+
trial_requires_payment_method?: boolean;
|
|
145
|
+
}
|
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
import 'server-only';
|
|
2
|
-
|
|
3
|
-
import { getSsgSupabaseClient } from '@nextblock-cms/db/server';
|
|
4
|
-
import {
|
|
5
|
-
customBlockDefinitionRowSchema,
|
|
6
|
-
type CustomBlockDefinition,
|
|
7
|
-
} from '@nextblock-cms/utils';
|
|
8
|
-
import { unstable_cache } from 'next/cache';
|
|
9
|
-
|
|
10
|
-
export const CUSTOM_BLOCK_DEFINITIONS_CACHE_TAG = 'custom-block-definitions';
|
|
11
|
-
const CUSTOM_BLOCK_SELECT = 'id, slug, name, description, fields, layout_schema, is_original';
|
|
12
|
-
|
|
13
|
-
export function getCustomBlockDefinitionCacheTag(idOrSlug: string) {
|
|
14
|
-
return `${CUSTOM_BLOCK_DEFINITIONS_CACHE_TAG}:${idOrSlug}`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function parseCustomBlockDefinitionRows(rows: unknown[]): CustomBlockDefinition[] {
|
|
18
|
-
return rows.map((row) => customBlockDefinitionRowSchema.parse(row));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function queryCustomBlockDefinitions() {
|
|
22
|
-
const supabase = getSsgSupabaseClient();
|
|
23
|
-
const { data, error } = await supabase
|
|
24
|
-
.from('custom_block_definitions')
|
|
25
|
-
.select(CUSTOM_BLOCK_SELECT)
|
|
26
|
-
.order('name', { ascending: true });
|
|
27
|
-
|
|
28
|
-
if (error) {
|
|
29
|
-
throw new Error(`Failed to load custom block definitions: ${error.message}`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return parseCustomBlockDefinitionRows(data ?? []);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const getCachedCustomBlockDefinitions = unstable_cache(
|
|
36
|
-
queryCustomBlockDefinitions,
|
|
37
|
-
[CUSTOM_BLOCK_DEFINITIONS_CACHE_TAG],
|
|
38
|
-
{
|
|
39
|
-
revalidate: 60,
|
|
40
|
-
tags: [CUSTOM_BLOCK_DEFINITIONS_CACHE_TAG],
|
|
41
|
-
}
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
const getCachedCustomBlockDefinitionBySlugInternal = unstable_cache(
|
|
45
|
-
async (slug: string) => {
|
|
46
|
-
const definitions = await queryCustomBlockDefinitions();
|
|
47
|
-
return definitions.find((definition) => definition.slug === slug) ?? null;
|
|
48
|
-
},
|
|
49
|
-
[`${CUSTOM_BLOCK_DEFINITIONS_CACHE_TAG}:by-slug`],
|
|
50
|
-
{
|
|
51
|
-
revalidate: 60,
|
|
52
|
-
tags: [CUSTOM_BLOCK_DEFINITIONS_CACHE_TAG],
|
|
53
|
-
}
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
async function queryCustomBlockDefinitionBySlug(slug: string): Promise<CustomBlockDefinition | null> {
|
|
57
|
-
const supabase = getSsgSupabaseClient();
|
|
58
|
-
const { data, error } = await supabase
|
|
59
|
-
.from('custom_block_definitions')
|
|
60
|
-
.select(CUSTOM_BLOCK_SELECT)
|
|
61
|
-
.eq('slug', slug)
|
|
62
|
-
.maybeSingle();
|
|
63
|
-
|
|
64
|
-
if (error) {
|
|
65
|
-
throw new Error(`Failed to load custom block definition "${slug}": ${error.message}`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return data ? customBlockDefinitionRowSchema.parse(data) : null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export async function getCachedCustomBlockDefinitionBySlug(slug: string) {
|
|
72
|
-
const cached = await getCachedCustomBlockDefinitionBySlugInternal(slug);
|
|
73
|
-
if (cached) {
|
|
74
|
-
return cached;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// The cached lookup can hold a stale "not found" result for a definition that
|
|
78
|
-
// was created or edited within the last revalidation window. Fall back to a
|
|
79
|
-
// direct read so a freshly saved custom block renders on the front end
|
|
80
|
-
// immediately instead of showing "Unsupported block type".
|
|
81
|
-
try {
|
|
82
|
-
return await queryCustomBlockDefinitionBySlug(slug);
|
|
83
|
-
} catch (error) {
|
|
84
|
-
console.error('[custom-block-definitions] Live definition fallback failed:', error);
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
1
|
+
import 'server-only';
|
|
2
|
+
|
|
3
|
+
import { getSsgSupabaseClient } from '@nextblock-cms/db/server';
|
|
4
|
+
import {
|
|
5
|
+
customBlockDefinitionRowSchema,
|
|
6
|
+
type CustomBlockDefinition,
|
|
7
|
+
} from '@nextblock-cms/utils';
|
|
8
|
+
import { unstable_cache } from 'next/cache';
|
|
9
|
+
|
|
10
|
+
export const CUSTOM_BLOCK_DEFINITIONS_CACHE_TAG = 'custom-block-definitions';
|
|
11
|
+
const CUSTOM_BLOCK_SELECT = 'id, slug, name, description, fields, layout_schema, is_original';
|
|
12
|
+
|
|
13
|
+
export function getCustomBlockDefinitionCacheTag(idOrSlug: string) {
|
|
14
|
+
return `${CUSTOM_BLOCK_DEFINITIONS_CACHE_TAG}:${idOrSlug}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseCustomBlockDefinitionRows(rows: unknown[]): CustomBlockDefinition[] {
|
|
18
|
+
return rows.map((row) => customBlockDefinitionRowSchema.parse(row));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function queryCustomBlockDefinitions() {
|
|
22
|
+
const supabase = getSsgSupabaseClient();
|
|
23
|
+
const { data, error } = await supabase
|
|
24
|
+
.from('custom_block_definitions')
|
|
25
|
+
.select(CUSTOM_BLOCK_SELECT)
|
|
26
|
+
.order('name', { ascending: true });
|
|
27
|
+
|
|
28
|
+
if (error) {
|
|
29
|
+
throw new Error(`Failed to load custom block definitions: ${error.message}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return parseCustomBlockDefinitionRows(data ?? []);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const getCachedCustomBlockDefinitions = unstable_cache(
|
|
36
|
+
queryCustomBlockDefinitions,
|
|
37
|
+
[CUSTOM_BLOCK_DEFINITIONS_CACHE_TAG],
|
|
38
|
+
{
|
|
39
|
+
revalidate: 60,
|
|
40
|
+
tags: [CUSTOM_BLOCK_DEFINITIONS_CACHE_TAG],
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const getCachedCustomBlockDefinitionBySlugInternal = unstable_cache(
|
|
45
|
+
async (slug: string) => {
|
|
46
|
+
const definitions = await queryCustomBlockDefinitions();
|
|
47
|
+
return definitions.find((definition) => definition.slug === slug) ?? null;
|
|
48
|
+
},
|
|
49
|
+
[`${CUSTOM_BLOCK_DEFINITIONS_CACHE_TAG}:by-slug`],
|
|
50
|
+
{
|
|
51
|
+
revalidate: 60,
|
|
52
|
+
tags: [CUSTOM_BLOCK_DEFINITIONS_CACHE_TAG],
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
async function queryCustomBlockDefinitionBySlug(slug: string): Promise<CustomBlockDefinition | null> {
|
|
57
|
+
const supabase = getSsgSupabaseClient();
|
|
58
|
+
const { data, error } = await supabase
|
|
59
|
+
.from('custom_block_definitions')
|
|
60
|
+
.select(CUSTOM_BLOCK_SELECT)
|
|
61
|
+
.eq('slug', slug)
|
|
62
|
+
.maybeSingle();
|
|
63
|
+
|
|
64
|
+
if (error) {
|
|
65
|
+
throw new Error(`Failed to load custom block definition "${slug}": ${error.message}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return data ? customBlockDefinitionRowSchema.parse(data) : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function getCachedCustomBlockDefinitionBySlug(slug: string) {
|
|
72
|
+
const cached = await getCachedCustomBlockDefinitionBySlugInternal(slug);
|
|
73
|
+
if (cached) {
|
|
74
|
+
return cached;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The cached lookup can hold a stale "not found" result for a definition that
|
|
78
|
+
// was created or edited within the last revalidation window. Fall back to a
|
|
79
|
+
// direct read so a freshly saved custom block renders on the front end
|
|
80
|
+
// immediately instead of showing "Unsupported block type".
|
|
81
|
+
try {
|
|
82
|
+
return await queryCustomBlockDefinitionBySlug(slug);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('[custom-block-definitions] Live definition fallback failed:', error);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|